Compare commits

..

No commits in common. "a6db3ddfa446b92554f71c93eb3f3379e866ee0c" and "68e3e38d75e0a0a4bee0cdc534920749c30b8add" have entirely different histories.

9 changed files with 227 additions and 277 deletions

View File

@ -17,8 +17,10 @@
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0", "@xterm/addon-search": "^0.16.0",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"naive-ui": "^2.40.0",
"pinia": "^2.2.0", "pinia": "^2.2.0",
"vue": "^3.5.0", "vue": "^3.5.0",
"vue-router": "^4.4.0" "vue-router": "^4.4.0"
@ -254,6 +256,30 @@
"w3c-keyname": "^2.2.4" "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": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@ -745,6 +771,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@lezer/common": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
@ -1520,6 +1552,21 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@vitejs/plugin-vue": {
"version": "5.2.4", "version": "5.2.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
@ -1729,6 +1776,12 @@
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==", "integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
"license": "MIT" "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": { "node_modules/@xterm/xterm": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
@ -1745,6 +1798,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1783,12 +1842,47 @@
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT" "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": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT" "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": { "node_modules/de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "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==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT" "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": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@ -1930,6 +2030,15 @@
"he": "bin/he" "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": { "node_modules/jiti": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@ -2213,6 +2322,18 @@
"url": "https://opencollective.com/parcel" "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": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@ -2245,6 +2366,38 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -2384,6 +2537,12 @@
"fsevents": "~2.3.2" "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": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "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" "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": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@ -2451,6 +2616,18 @@
"node": ">=14.17" "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": { "node_modules/vite": {
"version": "6.4.1", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "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": { "node_modules/vscode-uri": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
@ -2612,6 +2801,24 @@
"typescript": ">=5.0.0" "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": { "node_modules/w3c-keyname": {
"version": "2.2.8", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",

View File

@ -20,8 +20,10 @@
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0", "@xterm/addon-search": "^0.16.0",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"naive-ui": "^2.40.0",
"pinia": "^2.2.0", "pinia": "^2.2.0",
"vue": "^3.5.0", "vue": "^3.5.0",
"vue-router": "^4.4.0" "vue-router": "^4.4.0"

View File

@ -53,10 +53,7 @@
Theme: {{ activeThemeName }} Theme: {{ activeThemeName }}
</button> </button>
<span>UTF-8</span> <span>UTF-8</span>
<span v-if="sessionStore.activeDimensions"> <span>120&times;40</span>
{{ sessionStore.activeDimensions.cols }}&times;{{ sessionStore.activeDimensions.rows }}
</span>
<span v-else>120&times;40</span>
</div> </div>
</div> </div>
</template> </template>

View File

@ -85,24 +85,11 @@ function getSessionTags(session: Session): string[] {
/** Check if the connection for this session uses the root user. */ /** Check if the connection for this session uses the root user. */
function isRootUser(session: Session): boolean { 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); const conn = connectionStore.connections.find((c) => c.id === session.connectionId);
if (!conn) return false; if (!conn) return false;
// TODO: Get actual username from the credential or session
if (conn.options) { // For now, check mock data root user detection will come from the session/credential store
try { return false;
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;
} }
/** Return Tailwind classes for environment tag badges. */ /** Return Tailwind classes for environment tag badges. */

View File

@ -9,7 +9,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from "vue"; import { ref, onMounted, watch } from "vue";
import { useTerminal } from "@/composables/useTerminal"; import { useTerminal } from "@/composables/useTerminal";
import { useSessionStore } from "@/stores/session.store";
import "@/assets/css/terminal.css"; import "@/assets/css/terminal.css";
const props = defineProps<{ const props = defineProps<{
@ -17,7 +16,6 @@ const props = defineProps<{
isActive: boolean; isActive: boolean;
}>(); }>();
const sessionStore = useSessionStore();
const containerRef = ref<HTMLElement | null>(null); const containerRef = ref<HTMLElement | null>(null);
const { terminal, mount, fit } = useTerminal(props.sessionId); const { terminal, mount, fit } = useTerminal(props.sessionId);
@ -25,16 +23,6 @@ onMounted(() => {
if (containerRef.value) { if (containerRef.value) {
mount(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 // 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 { function handleFocus(): void {
terminal.focus(); terminal.focus();
} }

View File

@ -105,7 +105,6 @@
<div class="flex flex-1 min-h-0"> <div class="flex flex-1 min-h-0">
<!-- Sidebar --> <!-- Sidebar -->
<div <div
v-if="sidebarVisible"
class="flex flex-col bg-[var(--wraith-bg-secondary)] border-r border-[var(--wraith-border)] shrink-0" class="flex flex-col bg-[var(--wraith-bg-secondary)] border-r border-[var(--wraith-border)] shrink-0"
:style="{ width: sidebarWidth + 'px' }" :style="{ width: sidebarWidth + 'px' }"
> >
@ -188,36 +187,6 @@
<!-- Connection Edit Dialog (for File menu / Command Palette new connection) --> <!-- Connection Edit Dialog (for File menu / Command Palette new connection) -->
<ConnectionEditDialog ref="connectionEditDialog" /> <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> </div>
</template> </template>
@ -255,13 +224,9 @@ const sessionStore = useSessionStore();
// copilotStore removed // copilotStore removed
const sidebarWidth = ref(240); const sidebarWidth = ref(240);
const sidebarVisible = ref(true);
const sidebarTab = ref<SidebarTab>("connections"); const sidebarTab = ref<SidebarTab>("connections");
const quickConnectInput = ref(""); 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 // Auto-switch to SFTP tab when an SSH session becomes active
watch(() => sessionStore.activeSession, (session) => { watch(() => sessionStore.activeSession, (session) => {
if (session && session.protocol === "ssh") { if (session && session.protocol === "ssh") {
@ -329,8 +294,6 @@ async function handleOpenFile(entry: FileEntry): Promise<void> {
/** Handle theme selection from the ThemePicker. */ /** Handle theme selection from the ThemePicker. */
function handleThemeSelect(theme: ThemeDefinition): void { function handleThemeSelect(theme: ThemeDefinition): void {
statusBar.value?.setThemeName(theme.name); 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. */ /** Global keyboard shortcut handler. */
function handleKeydown(event: KeyboardEvent): void { function handleKeydown(event: KeyboardEvent): void {
// Skip shortcuts when the user is typing in an input, textarea, or select // Ctrl+K or Cmd+K open command palette
const target = event.target as HTMLElement; if ((event.ctrlKey || event.metaKey) && event.key === "k") {
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") {
event.preventDefault(); event.preventDefault();
commandPalette.value?.toggle(); 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); document.addEventListener("keydown", handleKeydown);
// Load connections and groups from the Go backend after vault unlock // Load connections and groups from the Go backend after vault unlock
await connectionStore.loadAll(); await connectionStore.loadAll();
// First-run: if no connections found, offer to import from MobaXTerm
if (connectionStore.connections.length === 0) {
showMobaPrompt.value = true;
}
}); });
onUnmounted(() => { onUnmounted(() => {

View File

@ -2,7 +2,6 @@ import { defineStore } from "pinia";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { Call } from "@wailsio/runtime"; import { Call } from "@wailsio/runtime";
import { useConnectionStore } from "@/stores/connection.store"; import { useConnectionStore } from "@/stores/connection.store";
import type { ThemeDefinition } from "@/components/common/ThemePicker.vue";
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp"; const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
@ -12,12 +11,6 @@ export interface Session {
name: string; name: string;
protocol: "ssh" | "rdp"; protocol: "ssh" | "rdp";
active: boolean; active: boolean;
username?: string;
}
export interface TerminalDimensions {
cols: number;
rows: number;
} }
export const useSessionStore = defineStore("session", () => { export const useSessionStore = defineStore("session", () => {
@ -26,12 +19,6 @@ export const useSessionStore = defineStore("session", () => {
const connecting = ref(false); const connecting = ref(false);
const lastError = ref<string | null>(null); 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(() => const activeSession = computed(() =>
sessions.value.find((s) => s.id === activeSessionId.value) ?? null, 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. * Connect to a server by connection ID.
* Multiple sessions to the same host are allowed (MobaXTerm-style). * Calls the real Go backend to establish an SSH or RDP session.
* Each gets its own tab with a disambiguated name like "Asgard (2)".
*/ */
async function connect(connectionId: number): Promise<void> { async function connect(connectionId: number): Promise<void> {
const connectionStore = useConnectionStore(); const connectionStore = useConnectionStore();
const conn = connectionStore.connections.find((c) => c.id === connectionId); const conn = connectionStore.connections.find((c) => c.id === connectionId);
if (!conn) return; 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; connecting.value = true;
try { try {
if (conn.protocol === "ssh") { if (conn.protocol === "ssh") {
let sessionId: string; let sessionId: string;
let resolvedUsername: string | undefined;
try { try {
// Try with stored credentials first // Try with stored credentials first
@ -112,7 +93,6 @@ export const useSessionStore = defineStore("session", () => {
const password = prompt(`Password for ${username}@${conn.hostname}:`); const password = prompt(`Password for ${username}@${conn.hostname}:`);
if (password === null) throw new Error("Connection cancelled"); if (password === null) throw new Error("Connection cancelled");
resolvedUsername = username;
sessionId = await Call.ByName( sessionId = await Call.ByName(
`${APP}.ConnectSSHWithPassword`, `${APP}.ConnectSSHWithPassword`,
connectionId, 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({ sessions.value.push({
id: sessionId, id: sessionId,
connectionId, connectionId,
name: disambiguatedName(conn.name, connectionId), name: conn.name,
protocol: "ssh", protocol: "ssh",
active: true, active: true,
username: resolvedUsername,
}); });
activeSessionId.value = sessionId; activeSessionId.value = sessionId;
} else if (conn.protocol === "rdp") { } else if (conn.protocol === "rdp") {
@ -157,7 +126,7 @@ export const useSessionStore = defineStore("session", () => {
sessions.value.push({ sessions.value.push({
id: sessionId, id: sessionId,
connectionId, connectionId,
name: disambiguatedName(conn.name, connectionId), name: conn.name,
protocol: "rdp", protocol: "rdp",
active: true, 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 { return {
sessions, sessions,
activeSessionId, activeSessionId,
@ -197,13 +150,8 @@ export const useSessionStore = defineStore("session", () => {
sessionCount, sessionCount,
connecting, connecting,
lastError, lastError,
activeTheme,
terminalDimensions,
activeDimensions,
activateSession, activateSession,
closeSession, closeSession,
connect, connect,
setTheme,
setTerminalDimensions,
}; };
}); });

View File

@ -45,7 +45,6 @@ type WraithApp struct {
Credentials *credentials.CredentialService Credentials *credentials.CredentialService
AI *ai.AIService AI *ai.AIService
Updater *updater.UpdateService Updater *updater.UpdateService
Workspace *WorkspaceService
oauthMgr *ai.OAuthManager oauthMgr *ai.OAuthManager
wailsApp *application.App wailsApp *application.App
unlocked bool unlocked bool
@ -80,31 +79,18 @@ func New(version string) (*WraithApp, error) {
themeSvc := theme.NewThemeService(database) themeSvc := theme.NewThemeService(database)
sessionMgr := session.NewManager() sessionMgr := session.NewManager()
pluginReg := plugin.NewRegistry() 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. // SSH output handler — emits Wails events to the frontend.
// The closure captures `app` (the WraithApp being built). The wailsApp // The closure captures `app` (the WraithApp being built). The wailsApp
// field is set after application.New() in main.go, but SSH sessions only // 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. // start after app.Run(), so wailsApp is always valid at call time.
var app *WraithApp var app *WraithApp
sshOutputHandler := func(sessionID string, data []byte) { sshSvc := ssh.NewSSHService(database, func(sessionID string, data []byte) {
if app != nil && app.wailsApp != nil { if app != nil && app.wailsApp != nil {
// Base64 encode binary data for safe transport over Wails events // Base64 encode binary data for safe transport over Wails events
app.wailsApp.Event.Emit("ssh:data:"+sessionID, base64.StdEncoding.EncodeToString(data)) 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() sftpSvc := sftp.NewSFTPService()
// RDP service with platform-aware backend factory. // RDP service with platform-aware backend factory.
@ -132,17 +118,6 @@ func New(version string) (*WraithApp, error) {
} }
updaterSvc := updater.NewUpdateService(version) 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{ app = &WraithApp{
db: database, db: database,
@ -156,7 +131,6 @@ func New(version string) (*WraithApp, error) {
RDP: rdpSvc, RDP: rdpSvc,
AI: aiSvc, AI: aiSvc,
Updater: updaterSvc, Updater: updaterSvc,
Workspace: workspaceSvc,
oauthMgr: oauthMgr, oauthMgr: oauthMgr,
} }
return app, nil 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) 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) slog.Info("SSH session started", "sessionID", sessionID, "host", conn.Hostname, "user", username)
return sessionID, nil return sessionID, nil
} }

View File

@ -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
);