Compare commits
No commits in common. "a6db3ddfa446b92554f71c93eb3f3379e866ee0c" and "68e3e38d75e0a0a4bee0cdc534920749c30b8add" have entirely different histories.
a6db3ddfa4
...
68e3e38d75
207
frontend/package-lock.json
generated
207
frontend/package-lock.json
generated
@ -17,8 +17,10 @@
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-search": "^0.16.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/addon-webgl": "^0.19.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"naive-ui": "^2.40.0",
|
||||
"pinia": "^2.2.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.4.0"
|
||||
@ -254,6 +256,30 @@
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@css-render/plugin-bem": {
|
||||
"version": "0.15.14",
|
||||
"resolved": "https://registry.npmjs.org/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz",
|
||||
"integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"css-render": "~0.15.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@css-render/vue3-ssr": {
|
||||
"version": "0.15.14",
|
||||
"resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz",
|
||||
"integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
@ -745,6 +771,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@juggle/resize-observer": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
|
||||
@ -1520,6 +1552,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
|
||||
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
@ -1729,6 +1776,12 @@
|
||||
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xterm/addon-webgl": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz",
|
||||
"integrity": "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
|
||||
@ -1745,6 +1798,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -1783,12 +1842,47 @@
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-render": {
|
||||
"version": "0.15.14",
|
||||
"resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.14.tgz",
|
||||
"integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/hash": "~0.8.0",
|
||||
"csstype": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/css-render/node_modules/csstype": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
|
||||
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns-tz": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
|
||||
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"date-fns": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
@ -1880,6 +1974,12 @@
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/evtd": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz",
|
||||
"integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@ -1930,6 +2030,15 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
@ -2213,6 +2322,18 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
|
||||
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@ -2245,6 +2366,38 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/naive-ui": {
|
||||
"version": "2.44.1",
|
||||
"resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.44.1.tgz",
|
||||
"integrity": "sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@css-render/plugin-bem": "^0.15.14",
|
||||
"@css-render/vue3-ssr": "^0.15.14",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"async-validator": "^4.2.5",
|
||||
"css-render": "^0.15.14",
|
||||
"csstype": "^3.1.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"evtd": "^0.2.4",
|
||||
"highlight.js": "^11.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"seemly": "^0.3.10",
|
||||
"treemate": "^0.3.11",
|
||||
"vdirs": "^0.1.8",
|
||||
"vooks": "^0.2.12",
|
||||
"vueuc": "^0.4.65"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@ -2384,6 +2537,12 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/seemly": {
|
||||
"version": "0.3.10",
|
||||
"resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.10.tgz",
|
||||
"integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@ -2437,6 +2596,12 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/treemate": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/treemate/-/treemate-0.3.11.tgz",
|
||||
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@ -2451,6 +2616,18 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/vdirs": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
|
||||
"integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"evtd": "^0.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
@ -2526,6 +2703,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vooks": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz",
|
||||
"integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"evtd": "^0.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
@ -2612,6 +2801,24 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vueuc": {
|
||||
"version": "0.4.65",
|
||||
"resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.65.tgz",
|
||||
"integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@css-render/vue3-ssr": "^0.15.10",
|
||||
"@juggle/resize-observer": "^3.3.1",
|
||||
"css-render": "^0.15.10",
|
||||
"evtd": "^0.2.4",
|
||||
"seemly": "^0.3.6",
|
||||
"vdirs": "^0.1.4",
|
||||
"vooks": "^0.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
|
||||
@ -20,8 +20,10 @@
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-search": "^0.16.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/addon-webgl": "^0.19.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"naive-ui": "^2.40.0",
|
||||
"pinia": "^2.2.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.4.0"
|
||||
|
||||
@ -53,10 +53,7 @@
|
||||
Theme: {{ activeThemeName }}
|
||||
</button>
|
||||
<span>UTF-8</span>
|
||||
<span v-if="sessionStore.activeDimensions">
|
||||
{{ sessionStore.activeDimensions.cols }}×{{ sessionStore.activeDimensions.rows }}
|
||||
</span>
|
||||
<span v-else>120×40</span>
|
||||
<span>120×40</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -85,24 +85,11 @@ function getSessionTags(session: Session): string[] {
|
||||
|
||||
/** Check if the connection for this session uses the root user. */
|
||||
function isRootUser(session: Session): boolean {
|
||||
// Check username stored on the session object (set during connect)
|
||||
if (session.username === "root") return true;
|
||||
|
||||
// Fall back to checking the connection's options JSON for a stored username
|
||||
const conn = connectionStore.connections.find((c) => c.id === session.connectionId);
|
||||
if (!conn) return false;
|
||||
|
||||
if (conn.options) {
|
||||
try {
|
||||
const opts = JSON.parse(conn.options);
|
||||
if (opts?.username === "root") return true;
|
||||
} catch {
|
||||
// ignore malformed options
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if "root" appears in the connection tags
|
||||
return conn.tags?.includes("root") ?? false;
|
||||
// TODO: Get actual username from the credential or session
|
||||
// For now, check mock data — root user detection will come from the session/credential store
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Return Tailwind classes for environment tag badges. */
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useTerminal } from "@/composables/useTerminal";
|
||||
import { useSessionStore } from "@/stores/session.store";
|
||||
import "@/assets/css/terminal.css";
|
||||
|
||||
const props = defineProps<{
|
||||
@ -17,7 +16,6 @@ const props = defineProps<{
|
||||
isActive: boolean;
|
||||
}>();
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const { terminal, mount, fit } = useTerminal(props.sessionId);
|
||||
|
||||
@ -25,16 +23,6 @@ onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
mount(containerRef.value);
|
||||
}
|
||||
|
||||
// Apply the current theme immediately if one is already active
|
||||
if (sessionStore.activeTheme) {
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
// Track terminal dimensions in the session store
|
||||
terminal.onResize(({ cols, rows }) => {
|
||||
sessionStore.setTerminalDimensions(props.sessionId, cols, rows);
|
||||
});
|
||||
});
|
||||
|
||||
// Re-fit and focus terminal when this tab becomes active
|
||||
@ -51,38 +39,6 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/** Apply the session store's active theme to this terminal instance. */
|
||||
function applyTheme(): void {
|
||||
const theme = sessionStore.activeTheme;
|
||||
if (!theme) return;
|
||||
terminal.options.theme = {
|
||||
background: theme.background,
|
||||
foreground: theme.foreground,
|
||||
cursor: theme.cursor,
|
||||
black: theme.black,
|
||||
red: theme.red,
|
||||
green: theme.green,
|
||||
yellow: theme.yellow,
|
||||
blue: theme.blue,
|
||||
magenta: theme.magenta,
|
||||
cyan: theme.cyan,
|
||||
white: theme.white,
|
||||
brightBlack: theme.brightBlack,
|
||||
brightRed: theme.brightRed,
|
||||
brightGreen: theme.brightGreen,
|
||||
brightYellow: theme.brightYellow,
|
||||
brightBlue: theme.brightBlue,
|
||||
brightMagenta: theme.brightMagenta,
|
||||
brightCyan: theme.brightCyan,
|
||||
brightWhite: theme.brightWhite,
|
||||
};
|
||||
}
|
||||
|
||||
// Watch for theme changes in the session store and apply to this terminal
|
||||
watch(() => sessionStore.activeTheme, (newTheme) => {
|
||||
if (newTheme) applyTheme();
|
||||
});
|
||||
|
||||
function handleFocus(): void {
|
||||
terminal.focus();
|
||||
}
|
||||
|
||||
@ -105,7 +105,6 @@
|
||||
<div class="flex flex-1 min-h-0">
|
||||
<!-- Sidebar -->
|
||||
<div
|
||||
v-if="sidebarVisible"
|
||||
class="flex flex-col bg-[var(--wraith-bg-secondary)] border-r border-[var(--wraith-border)] shrink-0"
|
||||
:style="{ width: sidebarWidth + 'px' }"
|
||||
>
|
||||
@ -188,36 +187,6 @@
|
||||
|
||||
<!-- Connection Edit Dialog (for File menu / Command Palette new connection) -->
|
||||
<ConnectionEditDialog ref="connectionEditDialog" />
|
||||
|
||||
<!-- First-run: MobaXTerm import prompt -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showMobaPrompt"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/50" @click="showMobaPrompt = false" />
|
||||
<div class="relative w-full max-w-sm bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl p-6 space-y-4">
|
||||
<h3 class="text-sm font-semibold text-[var(--wraith-text-primary)]">No connections found</h3>
|
||||
<p class="text-xs text-[var(--wraith-text-secondary)]">
|
||||
It looks like this is your first time running Wraith. Would you like to import connections from MobaXTerm?
|
||||
</p>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button
|
||||
class="px-3 py-1.5 text-xs rounded bg-[var(--wraith-bg-tertiary)] text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||
@click="showMobaPrompt = false"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-xs rounded bg-[#1f6feb] text-white hover:bg-[#388bfd] transition-colors cursor-pointer"
|
||||
@click="() => { showMobaPrompt = false; importDialog?.open(); }"
|
||||
>
|
||||
Import from MobaXTerm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -255,13 +224,9 @@ const sessionStore = useSessionStore();
|
||||
// copilotStore removed
|
||||
|
||||
const sidebarWidth = ref(240);
|
||||
const sidebarVisible = ref(true);
|
||||
const sidebarTab = ref<SidebarTab>("connections");
|
||||
const quickConnectInput = ref("");
|
||||
|
||||
/** Whether to show the MobaXTerm import prompt (first run, no connections). */
|
||||
const showMobaPrompt = ref(false);
|
||||
|
||||
// Auto-switch to SFTP tab when an SSH session becomes active
|
||||
watch(() => sessionStore.activeSession, (session) => {
|
||||
if (session && session.protocol === "ssh") {
|
||||
@ -329,8 +294,6 @@ async function handleOpenFile(entry: FileEntry): Promise<void> {
|
||||
/** Handle theme selection from the ThemePicker. */
|
||||
function handleThemeSelect(theme: ThemeDefinition): void {
|
||||
statusBar.value?.setThemeName(theme.name);
|
||||
// Propagate theme to all active terminal instances via the session store
|
||||
sessionStore.setTheme(theme);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -412,70 +375,10 @@ async function handleQuickConnect(): Promise<void> {
|
||||
|
||||
/** Global keyboard shortcut handler. */
|
||||
function handleKeydown(event: KeyboardEvent): void {
|
||||
// Skip shortcuts when the user is typing in an input, textarea, or select
|
||||
const target = event.target as HTMLElement;
|
||||
const isInputFocused = target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
|
||||
|
||||
const ctrl = event.ctrlKey || event.metaKey;
|
||||
|
||||
// Ctrl+K — open command palette (fires even in inputs to match VS Code behavior)
|
||||
if (ctrl && event.key === "k") {
|
||||
// Ctrl+K or Cmd+K — open command palette
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
|
||||
event.preventDefault();
|
||||
commandPalette.value?.toggle();
|
||||
return;
|
||||
}
|
||||
|
||||
// All remaining shortcuts skip when typing in input fields
|
||||
if (isInputFocused) return;
|
||||
|
||||
// Ctrl+W — close active tab
|
||||
if (ctrl && event.key === "w") {
|
||||
event.preventDefault();
|
||||
const active = sessionStore.activeSession;
|
||||
if (active) {
|
||||
sessionStore.closeSession(active.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Tab — next tab
|
||||
if (ctrl && event.key === "Tab" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
const sessions = sessionStore.sessions;
|
||||
if (sessions.length < 2) return;
|
||||
const idx = sessions.findIndex((s) => s.id === sessionStore.activeSessionId);
|
||||
const next = sessions[(idx + 1) % sessions.length];
|
||||
sessionStore.activateSession(next.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Shift+Tab — previous tab
|
||||
if (ctrl && event.key === "Tab" && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
const sessions = sessionStore.sessions;
|
||||
if (sessions.length < 2) return;
|
||||
const idx = sessions.findIndex((s) => s.id === sessionStore.activeSessionId);
|
||||
const prev = sessions[(idx - 1 + sessions.length) % sessions.length];
|
||||
sessionStore.activateSession(prev.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+1 through Ctrl+9 — switch to tab by index
|
||||
if (ctrl && event.key >= "1" && event.key <= "9") {
|
||||
const tabIndex = parseInt(event.key, 10) - 1;
|
||||
const sessions = sessionStore.sessions;
|
||||
if (tabIndex < sessions.length) {
|
||||
event.preventDefault();
|
||||
sessionStore.activateSession(sessions[tabIndex].id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+B — toggle sidebar
|
||||
if (ctrl && event.key === "b") {
|
||||
event.preventDefault();
|
||||
sidebarVisible.value = !sidebarVisible.value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,11 +386,6 @@ onMounted(async () => {
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
// Load connections and groups from the Go backend after vault unlock
|
||||
await connectionStore.loadAll();
|
||||
|
||||
// First-run: if no connections found, offer to import from MobaXTerm
|
||||
if (connectionStore.connections.length === 0) {
|
||||
showMobaPrompt.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@ -2,7 +2,6 @@ import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { Call } from "@wailsio/runtime";
|
||||
import { useConnectionStore } from "@/stores/connection.store";
|
||||
import type { ThemeDefinition } from "@/components/common/ThemePicker.vue";
|
||||
|
||||
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
|
||||
|
||||
@ -12,12 +11,6 @@ export interface Session {
|
||||
name: string;
|
||||
protocol: "ssh" | "rdp";
|
||||
active: boolean;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface TerminalDimensions {
|
||||
cols: number;
|
||||
rows: number;
|
||||
}
|
||||
|
||||
export const useSessionStore = defineStore("session", () => {
|
||||
@ -26,12 +19,6 @@ export const useSessionStore = defineStore("session", () => {
|
||||
const connecting = ref(false);
|
||||
const lastError = ref<string | null>(null);
|
||||
|
||||
/** Active terminal theme — applied to all terminal instances. */
|
||||
const activeTheme = ref<ThemeDefinition | null>(null);
|
||||
|
||||
/** Per-session terminal dimensions (cols x rows). */
|
||||
const terminalDimensions = ref<Record<string, TerminalDimensions>>({});
|
||||
|
||||
const activeSession = computed(() =>
|
||||
sessions.value.find((s) => s.id === activeSessionId.value) ?? null,
|
||||
);
|
||||
@ -67,32 +54,26 @@ export const useSessionStore = defineStore("session", () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** Count how many sessions already exist for this connection (for tab name disambiguation). */
|
||||
function sessionCountForConnection(connId: number): number {
|
||||
return sessions.value.filter((s) => s.connectionId === connId).length;
|
||||
}
|
||||
|
||||
/** Generate a disambiguated tab name like "Asgard", "Asgard (2)", "Asgard (3)". */
|
||||
function disambiguatedName(baseName: string, connId: number): string {
|
||||
const count = sessionCountForConnection(connId);
|
||||
return count === 0 ? baseName : `${baseName} (${count + 1})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a server by connection ID.
|
||||
* Multiple sessions to the same host are allowed (MobaXTerm-style).
|
||||
* Each gets its own tab with a disambiguated name like "Asgard (2)".
|
||||
* Calls the real Go backend to establish an SSH or RDP session.
|
||||
*/
|
||||
async function connect(connectionId: number): Promise<void> {
|
||||
const connectionStore = useConnectionStore();
|
||||
const conn = connectionStore.connections.find((c) => c.id === connectionId);
|
||||
if (!conn) return;
|
||||
|
||||
// Check if there's already an active session for this connection
|
||||
const existing = sessions.value.find((s) => s.connectionId === connectionId);
|
||||
if (existing) {
|
||||
activeSessionId.value = existing.id;
|
||||
return;
|
||||
}
|
||||
|
||||
connecting.value = true;
|
||||
try {
|
||||
if (conn.protocol === "ssh") {
|
||||
let sessionId: string;
|
||||
let resolvedUsername: string | undefined;
|
||||
|
||||
try {
|
||||
// Try with stored credentials first
|
||||
@ -112,7 +93,6 @@ export const useSessionStore = defineStore("session", () => {
|
||||
const password = prompt(`Password for ${username}@${conn.hostname}:`);
|
||||
if (password === null) throw new Error("Connection cancelled");
|
||||
|
||||
resolvedUsername = username;
|
||||
sessionId = await Call.ByName(
|
||||
`${APP}.ConnectSSHWithPassword`,
|
||||
connectionId,
|
||||
@ -126,23 +106,12 @@ export const useSessionStore = defineStore("session", () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get username from connection options if not already resolved
|
||||
if (!resolvedUsername && conn.options) {
|
||||
try {
|
||||
const opts = JSON.parse(conn.options);
|
||||
if (opts?.username) resolvedUsername = opts.username;
|
||||
} catch {
|
||||
// ignore malformed options
|
||||
}
|
||||
}
|
||||
|
||||
sessions.value.push({
|
||||
id: sessionId,
|
||||
connectionId,
|
||||
name: disambiguatedName(conn.name, connectionId),
|
||||
name: conn.name,
|
||||
protocol: "ssh",
|
||||
active: true,
|
||||
username: resolvedUsername,
|
||||
});
|
||||
activeSessionId.value = sessionId;
|
||||
} else if (conn.protocol === "rdp") {
|
||||
@ -157,7 +126,7 @@ export const useSessionStore = defineStore("session", () => {
|
||||
sessions.value.push({
|
||||
id: sessionId,
|
||||
connectionId,
|
||||
name: disambiguatedName(conn.name, connectionId),
|
||||
name: conn.name,
|
||||
protocol: "rdp",
|
||||
active: true,
|
||||
});
|
||||
@ -174,22 +143,6 @@ export const useSessionStore = defineStore("session", () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply a theme to all active terminal instances. */
|
||||
function setTheme(theme: ThemeDefinition): void {
|
||||
activeTheme.value = theme;
|
||||
}
|
||||
|
||||
/** Update the recorded dimensions for a terminal session. */
|
||||
function setTerminalDimensions(sessionId: string, cols: number, rows: number): void {
|
||||
terminalDimensions.value[sessionId] = { cols, rows };
|
||||
}
|
||||
|
||||
/** Get the dimensions for the active session, or null if not tracked yet. */
|
||||
const activeDimensions = computed<TerminalDimensions | null>(() => {
|
||||
if (!activeSessionId.value) return null;
|
||||
return terminalDimensions.value[activeSessionId.value] ?? null;
|
||||
});
|
||||
|
||||
return {
|
||||
sessions,
|
||||
activeSessionId,
|
||||
@ -197,13 +150,8 @@ export const useSessionStore = defineStore("session", () => {
|
||||
sessionCount,
|
||||
connecting,
|
||||
lastError,
|
||||
activeTheme,
|
||||
terminalDimensions,
|
||||
activeDimensions,
|
||||
activateSession,
|
||||
closeSession,
|
||||
connect,
|
||||
setTheme,
|
||||
setTerminalDimensions,
|
||||
};
|
||||
});
|
||||
|
||||
@ -45,7 +45,6 @@ type WraithApp struct {
|
||||
Credentials *credentials.CredentialService
|
||||
AI *ai.AIService
|
||||
Updater *updater.UpdateService
|
||||
Workspace *WorkspaceService
|
||||
oauthMgr *ai.OAuthManager
|
||||
wailsApp *application.App
|
||||
unlocked bool
|
||||
@ -80,31 +79,18 @@ func New(version string) (*WraithApp, error) {
|
||||
themeSvc := theme.NewThemeService(database)
|
||||
sessionMgr := session.NewManager()
|
||||
pluginReg := plugin.NewRegistry()
|
||||
pluginReg.RegisterImporter(&importer.MobaConfImporter{})
|
||||
|
||||
// Host key store — persists SSH host key fingerprints for TOFU verification
|
||||
hostKeyStore := ssh.NewHostKeyStore(database)
|
||||
|
||||
// SSH output handler — emits Wails events to the frontend.
|
||||
// The closure captures `app` (the WraithApp being built). The wailsApp
|
||||
// field is set after application.New() in main.go, but SSH sessions only
|
||||
// start after app.Run(), so wailsApp is always valid at call time.
|
||||
var app *WraithApp
|
||||
sshOutputHandler := func(sessionID string, data []byte) {
|
||||
sshSvc := ssh.NewSSHService(database, func(sessionID string, data []byte) {
|
||||
if app != nil && app.wailsApp != nil {
|
||||
// Base64 encode binary data for safe transport over Wails events
|
||||
app.wailsApp.Event.Emit("ssh:data:"+sessionID, base64.StdEncoding.EncodeToString(data))
|
||||
}
|
||||
}
|
||||
|
||||
// CWD handler — emits Wails events when the remote working directory changes
|
||||
sshCWDHandler := func(sessionID string, path string) {
|
||||
if app != nil && app.wailsApp != nil {
|
||||
app.wailsApp.Event.Emit("ssh:cwd:"+sessionID, path)
|
||||
}
|
||||
}
|
||||
|
||||
sshSvc := ssh.NewSSHService(database, hostKeyStore, sshOutputHandler, sshCWDHandler)
|
||||
})
|
||||
sftpSvc := sftp.NewSFTPService()
|
||||
|
||||
// RDP service with platform-aware backend factory.
|
||||
@ -132,17 +118,6 @@ func New(version string) (*WraithApp, error) {
|
||||
}
|
||||
|
||||
updaterSvc := updater.NewUpdateService(version)
|
||||
workspaceSvc := NewWorkspaceService(settingsSvc)
|
||||
|
||||
// Clear the clean shutdown flag on startup — it will be re-set on clean exit.
|
||||
// If it wasn't set, the previous run crashed and the workspace can be restored.
|
||||
wasClean := workspaceSvc.WasCleanShutdown()
|
||||
if err := workspaceSvc.ClearCleanShutdown(); err != nil {
|
||||
slog.Warn("failed to clear clean shutdown flag", "error", err)
|
||||
}
|
||||
if !wasClean {
|
||||
slog.Info("previous shutdown was not clean — workspace restore available")
|
||||
}
|
||||
|
||||
app = &WraithApp{
|
||||
db: database,
|
||||
@ -156,7 +131,6 @@ func New(version string) (*WraithApp, error) {
|
||||
RDP: rdpSvc,
|
||||
AI: aiSvc,
|
||||
Updater: updaterSvc,
|
||||
Workspace: workspaceSvc,
|
||||
oauthMgr: oauthMgr,
|
||||
}
|
||||
return app, nil
|
||||
@ -363,17 +337,6 @@ func (a *WraithApp) ConnectSSH(connectionID int64, cols, rows int) (string, erro
|
||||
slog.Warn("failed to update last_connected", "error", err)
|
||||
}
|
||||
|
||||
// Register with session manager
|
||||
if _, err := a.Sessions.Create(connectionID, "ssh"); err != nil {
|
||||
slog.Warn("failed to register SSH session in manager", "error", err)
|
||||
} else {
|
||||
// Store the SSH session ID as the manager session ID for lookup
|
||||
a.Sessions.SetState(sessionID, session.StateConnected)
|
||||
}
|
||||
|
||||
// Save workspace state after session change
|
||||
a.saveWorkspaceState()
|
||||
|
||||
slog.Info("SSH session started", "sessionID", sessionID, "host", conn.Hostname, "user", username)
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS connection_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
connection_id INTEGER NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
|
||||
protocol TEXT NOT NULL,
|
||||
connected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
disconnected_at DATETIME,
|
||||
duration_secs INTEGER
|
||||
);
|
||||
Loading…
Reference in New Issue
Block a user