Skip to content

Commit 2a5439d

Browse files
author
linyuan.yang
committed
支持终端
1 parent f7a3424 commit 2a5439d

15 files changed

Lines changed: 767 additions & 107 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"onlyBuiltDependencies": [
5151
"better-sqlite3",
5252
"esbuild",
53+
"node-pty",
5354
"protobufjs",
5455
"sqlite3",
5556
"sharp"

packages/chat-ui/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"@codemirror/state": "^6.5.2",
2020
"@codemirror/theme-one-dark": "^6.1.3",
2121
"@codemirror/view": "^6.40.0",
22+
"@xterm/addon-fit": "^0.11.0",
23+
"@xterm/addon-web-links": "^0.12.0",
24+
"@xterm/xterm": "^6.0.0",
2225
"dompurify": "^3.4.5",
2326
"marked": "^18.0.2",
2427
"sbot-ui": "workspace:*",

packages/chat-ui/src/WebSocketTransport.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IChatTransport } from './transport'
1+
import type { IChatTransport, ShellOption } from './transport'
22
import {
33
ChatEventType,
44
type ChatEvent,
@@ -243,4 +243,17 @@ export class WebSocketTransport implements IChatTransport {
243243
if (!res.ok) throw new Error(res.statusText)
244244
return res.json()
245245
}
246+
247+
// ── Pty (terminal) ──
248+
249+
async listShells(): Promise<ShellOption[]> {
250+
const res = await this.api('/api/pty/shells')
251+
return res?.data ?? res ?? []
252+
}
253+
254+
openPty(): WebSocket {
255+
const url = this._baseUrl ? new URL(this._baseUrl) : location
256+
const proto = url.protocol === 'https:' ? 'wss:' : 'ws:'
257+
return new WebSocket(`${proto}//${url.host}/ws/pty`)
258+
}
246259
}

packages/chat-ui/src/components/ChatView.vue

Lines changed: 61 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ const contentEl = ref<HTMLElement | null>(null)
6060
const isCompact = useCompactProvider(rootEl)
6161
const sidebarOpen = ref(false)
6262
const settingsOpen = ref(false)
63-
const explorerOpen = ref(false)
64-
const explorerWidth = ref(420)
65-
const explorerResizing = ref(false)
63+
const rightPanelOpen = ref(false)
64+
const rightPanelWidth = ref(420)
65+
const rightPanelResizing = ref(false)
6666
const sessionBarWidth = ref(180)
6767
const sessionBarResizing = ref(false)
6868
const sessionSearch = ref('')
@@ -115,10 +115,10 @@ const displayedMessages = computed<StoredMessage[]>(() =>
115115
showArchived.value ? messages.value : messages.value.filter(m => m.kind !== MessageKind.Archive),
116116
)
117117
118-
const explorerPaneStyle = computed(() =>
118+
const rightPanelStyle = computed(() =>
119119
isCompact.value || props.alwaysCompact
120120
? undefined
121-
: { width: `${explorerWidth.value}px` },
121+
: { width: `${rightPanelWidth.value}px` },
122122
)
123123
124124
// ── Event handler ──
@@ -275,39 +275,39 @@ function toggleSettings() {
275275
if (settingsOpen.value) sidebarOpen.value = false
276276
}
277277
278-
function toggleExplorer() {
279-
explorerOpen.value = !explorerOpen.value
280-
if (explorerOpen.value && (isCompact.value || props.alwaysCompact)) {
278+
function toggleRightPanel() {
279+
rightPanelOpen.value = !rightPanelOpen.value
280+
if (rightPanelOpen.value && (isCompact.value || props.alwaysCompact)) {
281281
sidebarOpen.value = false
282282
settingsOpen.value = false
283283
}
284284
}
285285
286-
function clampExplorerWidth(width: number): number {
286+
function clampRightPanelWidth(width: number): number {
287287
const contentWidth = contentEl.value?.clientWidth ?? 0
288288
if (contentWidth <= 0) return Math.max(280, width)
289289
const min = Math.min(280, Math.max(220, contentWidth * 0.35))
290290
const max = Math.max(min, contentWidth * 0.7)
291291
return Math.min(max, Math.max(min, width))
292292
}
293293
294-
function startExplorerResize(e: PointerEvent) {
294+
function startRightPanelResize(e: PointerEvent) {
295295
if (isCompact.value || props.alwaysCompact) return
296296
e.preventDefault()
297-
explorerResizing.value = true
298-
window.addEventListener('pointermove', onExplorerResize)
299-
window.addEventListener('pointerup', stopExplorerResize, { once: true })
297+
rightPanelResizing.value = true
298+
window.addEventListener('pointermove', onRightPanelResize)
299+
window.addEventListener('pointerup', stopRightPanelResize, { once: true })
300300
}
301301
302-
function onExplorerResize(e: PointerEvent) {
303-
if (!explorerResizing.value || !contentEl.value) return
302+
function onRightPanelResize(e: PointerEvent) {
303+
if (!rightPanelResizing.value || !contentEl.value) return
304304
const rect = contentEl.value.getBoundingClientRect()
305-
explorerWidth.value = clampExplorerWidth(rect.right - e.clientX)
305+
rightPanelWidth.value = clampRightPanelWidth(rect.right - e.clientX)
306306
}
307307
308-
function stopExplorerResize() {
309-
explorerResizing.value = false
310-
window.removeEventListener('pointermove', onExplorerResize)
308+
function stopRightPanelResize() {
309+
rightPanelResizing.value = false
310+
window.removeEventListener('pointermove', onRightPanelResize)
311311
}
312312
313313
function clampSessionBarWidth(width: number): number {
@@ -579,7 +579,7 @@ onMounted(async () => {
579579
onBeforeUnmount(() => {
580580
props.transport.offEvent(handleEvent)
581581
props.transport.disconnect()
582-
window.removeEventListener('pointermove', onExplorerResize)
582+
window.removeEventListener('pointermove', onRightPanelResize)
583583
window.removeEventListener('pointermove', onSessionBarResize)
584584
})
585585
</script>
@@ -686,13 +686,14 @@ onBeforeUnmount(() => {
686686
<template #actions-prepend>
687687
<slot name="status-actions" :session="activeSession" />
688688
<button
689-
class="chatui-explorer-toggle"
690-
:class="{ 'chatui-explorer-toggle--active': explorerOpen }"
691-
:title="L.explorerToggle"
692-
@click="toggleExplorer"
689+
class="chatui-right-panel-toggle"
690+
:class="{ 'chatui-right-panel-toggle--active': rightPanelOpen }"
691+
:title="L.rightPanelToggle"
692+
@click="toggleRightPanel"
693693
>
694-
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
695-
<path d="M2 3.5A1.5 1.5 0 0 1 3.5 2h3l1.5 1.5h4.5A1.5 1.5 0 0 1 14 5v7.5A1.5 1.5 0 0 1 12.5 14h-9A1.5 1.5 0 0 1 2 12.5v-9z"/>
694+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
695+
<rect x="2" y="3" width="12" height="10" rx="1.5"/>
696+
<line x1="10" y1="3" x2="10" y2="13"/>
696697
</svg>
697698
</button>
698699
</template>
@@ -748,20 +749,21 @@ onBeforeUnmount(() => {
748749
<template #actions-prepend>
749750
<slot name="status-actions" :session="activeSession" />
750751
<button
751-
class="chatui-explorer-toggle"
752-
:class="{ 'chatui-explorer-toggle--active': explorerOpen }"
753-
:title="L.explorerToggle"
754-
@click="toggleExplorer"
752+
class="chatui-right-panel-toggle"
753+
:class="{ 'chatui-right-panel-toggle--active': rightPanelOpen }"
754+
:title="L.rightPanelToggle"
755+
@click="toggleRightPanel"
755756
>
756-
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
757-
<path d="M2 3.5A1.5 1.5 0 0 1 3.5 2h3l1.5 1.5h4.5A1.5 1.5 0 0 1 14 5v7.5A1.5 1.5 0 0 1 12.5 14h-9A1.5 1.5 0 0 1 2 12.5v-9z"/>
757+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
758+
<rect x="2" y="3" width="12" height="10" rx="1.5"/>
759+
<line x1="10" y1="3" x2="10" y2="13"/>
758760
</svg>
759761
</button>
760762
</template>
761763
</StatusBar>
762764

763-
<!-- Chat area + optional Explorer -->
764-
<div ref="contentEl" class="chatui-content" :class="{ 'chatui-explorer-resizing': explorerResizing }">
765+
<!-- Chat area + optional right panel -->
766+
<div ref="contentEl" class="chatui-content" :class="{ 'chatui-right-panel-resizing': rightPanelResizing }">
765767
<ChatArea
766768
ref="chatAreaRef"
767769
class="chatui-chatarea"
@@ -784,16 +786,16 @@ onBeforeUnmount(() => {
784786
@answer="onAnswer"
785787
@abort="onAbort"
786788
/>
787-
<div v-if="explorerOpen" class="chatui-explorer-pane" :style="explorerPaneStyle">
789+
<div v-if="rightPanelOpen" class="chatui-right-panel-pane" :style="rightPanelStyle">
788790
<div
789791
v-if="!(isCompact || alwaysCompact)"
790-
class="chatui-explorer-resizer"
791-
:title="L.explorerToggle"
792-
@pointerdown="startExplorerResize"
792+
class="chatui-right-panel-resizer"
793+
:title="L.rightPanelToggle"
794+
@pointerdown="startRightPanelResize"
793795
/>
794-
<div v-if="isCompact || alwaysCompact" class="chatui-explorer-panel-header">
795-
<span class="chatui-explorer-panel-title">{{ L.explorerToggle }}</span>
796-
<button class="chatui-explorer-panel-close" :title="L.close" @click="explorerOpen = false">×</button>
796+
<div v-if="isCompact || alwaysCompact" class="chatui-right-panel-header">
797+
<span class="chatui-right-panel-title">{{ L.rightPanelToggle }}</span>
798+
<button class="chatui-right-panel-close" :title="L.close" @click="rightPanelOpen = false">×</button>
797799
</div>
798800
<RightPanel :transport="transport" :root="activeSession?.workPath" :labels="labels" editable />
799801
</div>
@@ -828,13 +830,13 @@ onBeforeUnmount(() => {
828830
min-width: 0;
829831
}
830832
831-
/* Chat area + Explorer side-by-side */
833+
/* Chat area + right panel side-by-side */
832834
.chatui-content {
833835
flex: 1; display: flex; overflow: hidden; min-height: 0;
834836
position: relative;
835837
}
836-
.chatui-explorer-resizing,
837-
.chatui-explorer-resizing * {
838+
.chatui-right-panel-resizing,
839+
.chatui-right-panel-resizing * {
838840
cursor: col-resize !important;
839841
user-select: none;
840842
}
@@ -870,7 +872,7 @@ onBeforeUnmount(() => {
870872
.chatui-chatarea {
871873
flex: 1; min-width: 0;
872874
}
873-
.chatui-explorer-pane {
875+
.chatui-right-panel-pane {
874876
min-width: 280px;
875877
max-width: 70%;
876878
border-left: 1px solid var(--chatui-border);
@@ -881,8 +883,8 @@ onBeforeUnmount(() => {
881883
position: relative;
882884
flex-shrink: 0;
883885
}
884-
.chatui-explorer-pane > .chatui-explorer { flex: 1; min-width: 0; }
885-
.chatui-explorer-resizer {
886+
.chatui-right-panel-pane > :deep(.chatui-right-panel) { flex: 1; min-width: 0; }
887+
.chatui-right-panel-resizer {
886888
position: absolute;
887889
top: 0;
888890
bottom: 0;
@@ -892,7 +894,7 @@ onBeforeUnmount(() => {
892894
cursor: col-resize;
893895
touch-action: none;
894896
}
895-
.chatui-explorer-resizer::after {
897+
.chatui-right-panel-resizer::after {
896898
content: '';
897899
position: absolute;
898900
top: 0;
@@ -902,11 +904,11 @@ onBeforeUnmount(() => {
902904
background: transparent;
903905
transition: background 0.15s;
904906
}
905-
.chatui-explorer-resizer:hover::after,
906-
.chatui-explorer-resizing .chatui-explorer-resizer::after {
907+
.chatui-right-panel-resizer:hover::after,
908+
.chatui-right-panel-resizing .chatui-right-panel-resizer::after {
907909
background: var(--chatui-border-focus, var(--chatui-accent));
908910
}
909-
.chatui-explorer-panel-header {
911+
.chatui-right-panel-header {
910912
display: flex;
911913
align-items: center;
912914
gap: 8px;
@@ -916,7 +918,7 @@ onBeforeUnmount(() => {
916918
background: var(--chatui-bg-surface);
917919
flex-shrink: 0;
918920
}
919-
.chatui-explorer-panel-title {
921+
.chatui-right-panel-title {
920922
flex: 1;
921923
min-width: 0;
922924
overflow: hidden;
@@ -926,7 +928,7 @@ onBeforeUnmount(() => {
926928
font-weight: 600;
927929
color: var(--chatui-fg);
928930
}
929-
.chatui-explorer-panel-close {
931+
.chatui-right-panel-close {
930932
width: 24px;
931933
height: 24px;
932934
display: inline-flex;
@@ -940,12 +942,12 @@ onBeforeUnmount(() => {
940942
font-size: 18px;
941943
line-height: 1;
942944
}
943-
.chatui-explorer-panel-close:hover {
945+
.chatui-right-panel-close:hover {
944946
background: var(--chatui-bg-hover);
945947
color: var(--chatui-fg);
946948
}
947949
948-
.chatui-explorer-toggle {
950+
.chatui-right-panel-toggle {
949951
display: inline-flex; align-items: center; justify-content: center;
950952
background: transparent; border: 1px solid var(--chatui-border); cursor: pointer;
951953
color: var(--chatui-fg-secondary);
@@ -958,18 +960,18 @@ onBeforeUnmount(() => {
958960
line-height: 1;
959961
transition: background 0.15s, color 0.15s;
960962
}
961-
.chatui-explorer-toggle:hover {
963+
.chatui-right-panel-toggle:hover {
962964
background: var(--chatui-bg-hover);
963965
color: var(--chatui-fg);
964966
}
965-
.chatui-explorer-toggle--active {
967+
.chatui-right-panel-toggle--active {
966968
background: var(--chatui-bg-active);
967969
color: var(--chatui-fg);
968970
border-color: var(--chatui-border-focus, var(--chatui-accent));
969971
}
970972
971-
/* Compact: explorer overlays the chat content instead of shrinking it. */
972-
.chatui-root.chatui-compact .chatui-explorer-pane {
973+
/* Compact: right panel overlays the chat content instead of shrinking it. */
974+
.chatui-root.chatui-compact .chatui-right-panel-pane {
973975
position: absolute;
974976
inset: 0;
975977
z-index: 80;

0 commit comments

Comments
 (0)