feat(v2): add SSH management, ctx integration, themes, detached mode, auto-updater
SSH session management: - SshSession struct + ssh_sessions SQLite table in session.rs - CRUD Tauri commands (ssh_session_list/save/delete) in lib.rs - SshDialog.svelte (create/edit modal), SshSessionList.svelte (sidebar) - SSH pane routes to TerminalPane with shell=/usr/bin/ssh + args ctx context database integration: - ctx.rs: read-only CtxDb (SQLITE_OPEN_READ_ONLY for ~/.claude-context/context.db) - 5 Tauri commands (ctx_list_projects/get_context/get_shared/get_summaries/search) - ContextPane.svelte with project selector, tabs, search - ctx-bridge.ts adapter Catppuccin theme flavors (Latte/Frappe/Macchiato/Mocha): - themes.ts: all 4 palette definitions + buildXtermTheme/applyCssVariables - theme.svelte.ts: reactive store with SQLite persistence - SettingsDialog flavor dropdown, TerminalPane theme-aware Detached pane mode (pop-out windows): - detach.ts: isDetachedMode/getDetachedConfig from URL params - App.svelte: conditional rendering of single pane without chrome Other additions: - Shiki syntax highlighting (highlight.ts, lazy singleton, 13 languages) - Tauri auto-updater plugin (tauri-plugin-updater + updater.ts) - AgentPane markdown rendering with Shiki code highlighting - New deps: shiki, @tauri-apps/plugin-updater, tauri-plugin-updater
This commit is contained in:
parent
4f2614186d
commit
4db7ccff60
28 changed files with 2992 additions and 51 deletions
571
v2/package-lock.json
generated
571
v2/package-lock.json
generated
|
|
@ -9,10 +9,12 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"marked": "^17.0.4"
|
||||
"marked": "^17.0.4",
|
||||
"shiki": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
|
|
@ -866,6 +868,106 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.1.tgz",
|
||||
"integrity": "sha512-vWvqi9JNgz1dRL9Nvog5wtx7RuNkf7MEPl2mU/cyUUxJeH1CAr3t+81h8zO8zs7DK6cKLMoU9TvukWIDjP4Lzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/primitive": "4.0.1",
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"hast-util-to-html": "^9.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-javascript": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.1.tgz",
|
||||
"integrity": "sha512-DJK9NiwtGYqMuKCRO4Ip0FKNDQpmaiS+K5bFjJ7DWFn4zHueDWgaUG8kAofkrnXF6zPPYYQY7J5FYVW9MbZyBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"oniguruma-to-es": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-oniguruma": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.1.tgz",
|
||||
"integrity": "sha512-oCWdCTDch3J8Kc0OZJ98KuUPC02O1VqIE3W/e2uvrHqTxYRR21RGEJMtchrgrxhsoJJCzmIciKsqG+q/yD+Cxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.1.tgz",
|
||||
"integrity": "sha512-v/mluaybWdnGJR4GqAR6zh8qAZohW9k+cGYT28Y7M8+jLbC0l4yG085O1A+WkseHTn+awd+P3UBymb2+MXFc8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/primitive": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.1.tgz",
|
||||
"integrity": "sha512-ns0hHZc5eWZuvuIEJz2pTx3Qecz0aRVYumVQJ8JgWY2tq/dH8WxdcVM49Fc2NsHEILNIT6vfdW9MF26RANWiTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.1.tgz",
|
||||
"integrity": "sha512-FW41C/D6j/yKQkzVdjrRPiJCtgeDaYRJFEyCKFCINuRJRj9WcmubhP4KQHPZ4+9eT87jruSrYPyoblNRyDFzvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/types": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.1.tgz",
|
||||
"integrity": "sha512-EaygPEn57+jJ76mw+nTLvIpJMAcMPokFbrF8lufsZP7Ukk+ToJYEcswN1G0e49nUZAq7aCQtoeW219A8HK1ZOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/vscode-textmate": {
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
|
||||
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sveltejs/acorn-typescript": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
|
||||
|
|
@ -925,6 +1027,15 @@
|
|||
"url": "https://opencollective.com/tauri"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-updater": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.0.tgz",
|
||||
"integrity": "sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/svelte": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz",
|
||||
|
|
@ -939,6 +1050,24 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
||||
|
|
@ -956,6 +1085,18 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@xterm/addon-canvas": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-canvas/-/addon-canvas-0.7.0.tgz",
|
||||
|
|
@ -1013,6 +1154,36 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-html4": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
||||
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-legacy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
||||
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
|
|
@ -1039,6 +1210,16 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
|
|
@ -1049,6 +1230,15 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
|
|
@ -1056,6 +1246,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
|
|
@ -1148,6 +1351,52 @@
|
|||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-html": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
||||
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"html-void-elements": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"stringify-entities": "^4.0.0",
|
||||
"zwitch": "^2.0.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-whitespace": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
||||
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-void-elements": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
|
|
@ -1187,6 +1436,116 @@
|
|||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
|
||||
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@ungap/structured-clone": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"trim-lines": "^3.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-character": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
|
||||
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-encode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
|
||||
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-sanitize-uri": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
|
||||
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-encode": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-symbol": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
|
||||
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
|
||||
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
|
|
@ -1227,6 +1586,23 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oniguruma-parser": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
|
||||
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oniguruma-to-es": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz",
|
||||
"integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"oniguruma-parser": "^0.12.1",
|
||||
"regex": "^6.0.1",
|
||||
"regex-recursion": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -1276,6 +1652,16 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
|
|
@ -1290,6 +1676,30 @@
|
|||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
|
||||
"integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regex-utilities": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regex-recursion": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
|
||||
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regex-utilities": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regex-utilities": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
|
||||
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
|
|
@ -1348,6 +1758,25 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.1.tgz",
|
||||
"integrity": "sha512-EkAEhDTN5WhpoQFXFw79OHIrSAfHhlImeCdSyg4u4XvrpxKEmdo/9x/HWSowujAnUrFsGOwWiE58a6GVentMnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "4.0.1",
|
||||
"@shikijs/engine-javascript": "4.0.1",
|
||||
"@shikijs/engine-oniguruma": "4.0.1",
|
||||
"@shikijs/langs": "4.0.1",
|
||||
"@shikijs/themes": "4.0.1",
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
|
@ -1358,6 +1787,30 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/space-separated-tokens": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
||||
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/stringify-entities": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities-html4": "^2.0.0",
|
||||
"character-entities-legacy": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.53.7",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz",
|
||||
|
|
@ -1427,6 +1880,16 @@
|
|||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
|
|
@ -1448,6 +1911,102 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unist-util-is": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
|
||||
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
||||
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-visit": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
|
||||
"integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-is": "^6.0.0",
|
||||
"unist-util-visit-parents": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-visit-parents": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
|
||||
"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-is": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile-message": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
||||
"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
|
|
@ -1549,6 +2108,16 @@
|
|||
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"marked": "^17.0.4"
|
||||
"marked": "^17.0.4",
|
||||
"shiki": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
360
v2/src-tauri/Cargo.lock
generated
360
v2/src-tauri/Cargo.lock
generated
|
|
@ -87,6 +87,15 @@ version = "1.0.102"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
|
|
@ -243,6 +252,7 @@ dependencies = [
|
|||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-updater",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
@ -626,6 +636,17 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
|
|
@ -826,6 +847,16 @@ dependencies = [
|
|||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
|
|
@ -838,6 +869,12 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
|
|
@ -1493,6 +1530,22 @@ dependencies = [
|
|||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
|
|
@ -1942,6 +1995,12 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
|
|
@ -2033,6 +2092,12 @@ version = "0.3.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
|
|
@ -2278,6 +2343,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
|
@ -2293,6 +2359,18 @@ dependencies = [
|
|||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
|
|
@ -2337,12 +2415,32 @@ version = "1.21.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
|
|
@ -2965,15 +3063,20 @@ dependencies = [
|
|||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
|
|
@ -2985,6 +3088,20 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rkyv"
|
||||
version = "0.7.46"
|
||||
|
|
@ -3053,6 +3170,92 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
|
|
@ -3068,6 +3271,15 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
|
|
@ -3131,6 +3343,29 @@ version = "4.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.24.0"
|
||||
|
|
@ -3523,6 +3758,12 @@ version = "0.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
|
|
@ -3644,6 +3885,17 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
|
|
@ -3803,6 +4055,39 @@ dependencies = [
|
|||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs 6.0.0",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.10.1"
|
||||
|
|
@ -3903,6 +4188,19 @@ dependencies = [
|
|||
"toml 0.9.12+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tendril"
|
||||
version = "0.4.3"
|
||||
|
|
@ -4035,6 +4333,16 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
|
|
@ -4316,6 +4624,12 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
|
|
@ -4624,6 +4938,15 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
|
|
@ -4863,6 +5186,15 @@ dependencies = [
|
|||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
|
|
@ -5357,6 +5689,16 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
|
|
@ -5421,6 +5763,12 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
|
|
@ -5454,6 +5802,18 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.13.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
|
|
|||
|
|
@ -27,3 +27,4 @@ uuid = { version = "1", features = ["v4"] }
|
|||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
dirs = "5"
|
||||
notify = { version = "6", features = ["macos_fsevent"] }
|
||||
tauri-plugin-updater = "2.10.0"
|
||||
|
|
|
|||
172
v2/src-tauri/src/ctx.rs
Normal file
172
v2/src-tauri/src/ctx.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// ctx — Read-only access to the Claude Code context manager database
|
||||
// Database: ~/.claude-context/context.db (managed by ctx CLI tool)
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::Serialize;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CtxProject {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub work_dir: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CtxEntry {
|
||||
pub project: String,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CtxSummary {
|
||||
pub project: String,
|
||||
pub summary: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
pub struct CtxDb {
|
||||
conn: Mutex<Option<Connection>>,
|
||||
}
|
||||
|
||||
impl CtxDb {
|
||||
pub fn new() -> Self {
|
||||
let db_path = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".claude-context")
|
||||
.join("context.db");
|
||||
|
||||
let conn = if db_path.exists() {
|
||||
Connection::open_with_flags(
|
||||
&db_path,
|
||||
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self { conn: Mutex::new(conn) }
|
||||
}
|
||||
|
||||
pub fn list_projects(&self) -> Result<Vec<CtxProject>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT name, description, work_dir, created_at FROM sessions ORDER BY name")
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?;
|
||||
|
||||
let projects = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(CtxProject {
|
||||
name: row.get(0)?,
|
||||
description: row.get(1)?,
|
||||
work_dir: row.get(2)?,
|
||||
created_at: row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
pub fn get_context(&self, project: &str) -> Result<Vec<CtxEntry>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT project, key, value, updated_at FROM contexts WHERE project = ?1 ORDER BY key")
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?;
|
||||
|
||||
let entries = stmt
|
||||
.query_map(params![project], |row| {
|
||||
Ok(CtxEntry {
|
||||
project: row.get(0)?,
|
||||
key: row.get(1)?,
|
||||
value: row.get(2)?,
|
||||
updated_at: row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn get_shared(&self) -> Result<Vec<CtxEntry>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT key, value, updated_at FROM shared ORDER BY key")
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?;
|
||||
|
||||
let entries = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(CtxEntry {
|
||||
project: "shared".to_string(),
|
||||
key: row.get(0)?,
|
||||
value: row.get(1)?,
|
||||
updated_at: row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn get_summaries(&self, project: &str, limit: i64) -> Result<Vec<CtxSummary>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT project, summary, created_at FROM summaries WHERE project = ?1 ORDER BY created_at DESC LIMIT ?2")
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?;
|
||||
|
||||
let summaries = stmt
|
||||
.query_map(params![project, limit], |row| {
|
||||
Ok(CtxSummary {
|
||||
project: row.get(0)?,
|
||||
summary: row.get(1)?,
|
||||
created_at: row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(summaries)
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str) -> Result<Vec<CtxEntry>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT project, key, value FROM contexts_fts WHERE contexts_fts MATCH ?1 LIMIT 50")
|
||||
.map_err(|e| format!("ctx search failed: {e}"))?;
|
||||
|
||||
let entries = stmt
|
||||
.query_map(params![query], |row| {
|
||||
Ok(CtxEntry {
|
||||
project: row.get(0)?,
|
||||
key: row.get(1)?,
|
||||
value: row.get(2)?,
|
||||
updated_at: String::new(),
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx search failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
mod ctx;
|
||||
mod pty;
|
||||
mod sidecar;
|
||||
mod watcher;
|
||||
mod session;
|
||||
|
||||
use ctx::CtxDb;
|
||||
use pty::{PtyManager, PtyOptions};
|
||||
use session::{Session, SessionDb, LayoutState};
|
||||
use session::{Session, SessionDb, LayoutState, SshSession};
|
||||
use sidecar::{AgentQueryOptions, SidecarManager};
|
||||
use watcher::FileWatcherManager;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -15,6 +17,7 @@ struct AppState {
|
|||
sidecar_manager: Arc<SidecarManager>,
|
||||
session_db: Arc<SessionDb>,
|
||||
file_watcher: Arc<FileWatcherManager>,
|
||||
ctx_db: Arc<CtxDb>,
|
||||
}
|
||||
|
||||
// --- PTY commands ---
|
||||
|
|
@ -149,6 +152,50 @@ fn settings_list(state: State<'_, AppState>) -> Result<Vec<(String, String)>, St
|
|||
state.session_db.get_all_settings()
|
||||
}
|
||||
|
||||
// --- SSH session commands ---
|
||||
|
||||
#[tauri::command]
|
||||
fn ssh_session_list(state: State<'_, AppState>) -> Result<Vec<SshSession>, String> {
|
||||
state.session_db.list_ssh_sessions()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ssh_session_save(state: State<'_, AppState>, session: SshSession) -> Result<(), String> {
|
||||
state.session_db.save_ssh_session(&session)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ssh_session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
state.session_db.delete_ssh_session(&id)
|
||||
}
|
||||
|
||||
// --- ctx commands ---
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_list_projects(state: State<'_, AppState>) -> Result<Vec<ctx::CtxProject>, String> {
|
||||
state.ctx_db.list_projects()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_get_context(state: State<'_, AppState>, project: String) -> Result<Vec<ctx::CtxEntry>, String> {
|
||||
state.ctx_db.get_context(&project)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_get_shared(state: State<'_, AppState>) -> Result<Vec<ctx::CtxEntry>, String> {
|
||||
state.ctx_db.get_shared()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_get_summaries(state: State<'_, AppState>, project: String, limit: i64) -> Result<Vec<ctx::CtxSummary>, String> {
|
||||
state.ctx_db.get_summaries(&project, limit)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_search(state: State<'_, AppState>, query: String) -> Result<Vec<ctx::CtxEntry>, String> {
|
||||
state.ctx_db.search(&query)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let pty_manager = Arc::new(PtyManager::new());
|
||||
|
|
@ -163,12 +210,14 @@ pub fn run() {
|
|||
);
|
||||
|
||||
let file_watcher = Arc::new(FileWatcherManager::new());
|
||||
let ctx_db = Arc::new(CtxDb::new());
|
||||
|
||||
let app_state = AppState {
|
||||
pty_manager,
|
||||
sidecar_manager: sidecar_manager.clone(),
|
||||
session_db,
|
||||
file_watcher,
|
||||
ctx_db,
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
|
|
@ -195,7 +244,16 @@ pub fn run() {
|
|||
settings_get,
|
||||
settings_set,
|
||||
settings_list,
|
||||
ssh_session_list,
|
||||
ssh_session_save,
|
||||
ssh_session_delete,
|
||||
ctx_list_projects,
|
||||
ctx_get_context,
|
||||
ctx_get_shared,
|
||||
ctx_get_summaries,
|
||||
ctx_search,
|
||||
])
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.setup(move |app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,20 @@ use serde::{Deserialize, Serialize};
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SshSession {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub host: String,
|
||||
pub port: i32,
|
||||
pub username: String,
|
||||
pub key_file: String,
|
||||
pub folder: String,
|
||||
pub color: String,
|
||||
pub created_at: i64,
|
||||
pub last_used_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
|
|
@ -73,6 +87,19 @@ impl SessionDb {
|
|||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 22,
|
||||
username TEXT NOT NULL,
|
||||
key_file TEXT DEFAULT '',
|
||||
folder TEXT DEFAULT '',
|
||||
color TEXT DEFAULT '#89b4fa',
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER NOT NULL
|
||||
);
|
||||
"
|
||||
).map_err(|e| format!("Migration failed: {e}"))?;
|
||||
Ok(())
|
||||
|
|
@ -212,4 +239,80 @@ impl SessionDb {
|
|||
Ok(LayoutState { preset, pane_ids })
|
||||
}).map_err(|e| format!("Layout read failed: {e}"))
|
||||
}
|
||||
|
||||
// --- SSH session methods ---
|
||||
|
||||
pub fn list_ssh_sessions(&self) -> Result<Vec<SshSession>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, name, host, port, username, key_file, folder, color, created_at, last_used_at FROM ssh_sessions ORDER BY last_used_at DESC")
|
||||
.map_err(|e| format!("SSH query prepare failed: {e}"))?;
|
||||
|
||||
let sessions = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(SshSession {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
host: row.get(2)?,
|
||||
port: row.get(3)?,
|
||||
username: row.get(4)?,
|
||||
key_file: row.get(5)?,
|
||||
folder: row.get(6)?,
|
||||
color: row.get(7)?,
|
||||
created_at: row.get(8)?,
|
||||
last_used_at: row.get(9)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("SSH query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("SSH row read failed: {e}"))?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
pub fn save_ssh_session(&self, session: &SshSession) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO ssh_sessions (id, name, host, port, username, key_file, folder, color, created_at, last_used_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||
params![
|
||||
session.id,
|
||||
session.name,
|
||||
session.host,
|
||||
session.port,
|
||||
session.username,
|
||||
session.key_file,
|
||||
session.folder,
|
||||
session.color,
|
||||
session.created_at,
|
||||
session.last_used_at,
|
||||
],
|
||||
).map_err(|e| format!("SSH insert failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_ssh_session(&self, id: &str) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM ssh_sessions WHERE id = ?1", params![id])
|
||||
.map_err(|e| format!("SSH delete failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_ssh_session(&self, session: &SshSession) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE ssh_sessions SET name = ?1, host = ?2, port = ?3, username = ?4, key_file = ?5, folder = ?6, color = ?7, last_used_at = ?8 WHERE id = ?9",
|
||||
params![
|
||||
session.name,
|
||||
session.host,
|
||||
session.port,
|
||||
session.username,
|
||||
session.key_file,
|
||||
session.folder,
|
||||
session.color,
|
||||
session.last_used_at,
|
||||
session.id,
|
||||
],
|
||||
).map_err(|e| format!("SSH update failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,15 @@
|
|||
"csp": null
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"https://github.com/DexterFromLab/BTerminal/releases/latest/download/latest.json"
|
||||
],
|
||||
"dialog": true,
|
||||
"pubkey": ""
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "appimage"],
|
||||
|
|
|
|||
|
|
@ -6,8 +6,14 @@
|
|||
import ToastContainer from './lib/components/Notifications/ToastContainer.svelte';
|
||||
import SettingsDialog from './lib/components/Settings/SettingsDialog.svelte';
|
||||
import { addPane, focusPaneByIndex, removePane, getPanes, restoreFromDb } from './lib/stores/layout.svelte';
|
||||
import { initTheme } from './lib/stores/theme.svelte';
|
||||
import { isDetachedMode, getDetachedConfig } from './lib/utils/detach';
|
||||
import TerminalPane from './lib/components/Terminal/TerminalPane.svelte';
|
||||
import AgentPane from './lib/components/Agent/AgentPane.svelte';
|
||||
|
||||
let settingsOpen = $state(false);
|
||||
let detached = isDetachedMode();
|
||||
let detachedConfig = getDetachedConfig();
|
||||
import { startAgentDispatcher, stopAgentDispatcher } from './lib/agent-dispatcher';
|
||||
|
||||
function newTerminal() {
|
||||
|
|
@ -31,8 +37,9 @@
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
initTheme();
|
||||
startAgentDispatcher();
|
||||
restoreFromDb();
|
||||
if (!detached) restoreFromDb();
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
// Ctrl+N — new terminal
|
||||
|
|
@ -80,17 +87,42 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<aside class="sidebar">
|
||||
<SessionList />
|
||||
</aside>
|
||||
<main class="workspace">
|
||||
<TilingGrid />
|
||||
</main>
|
||||
<StatusBar />
|
||||
{#if detached && detachedConfig}
|
||||
<div class="detached-pane">
|
||||
{#if detachedConfig.type === 'terminal' || detachedConfig.type === 'ssh'}
|
||||
<TerminalPane
|
||||
shell={detachedConfig.shell}
|
||||
cwd={detachedConfig.cwd}
|
||||
args={detachedConfig.args}
|
||||
/>
|
||||
{:else if detachedConfig.type === 'agent'}
|
||||
<AgentPane
|
||||
sessionId={detachedConfig.sessionId ?? crypto.randomUUID()}
|
||||
cwd={detachedConfig.cwd}
|
||||
/>
|
||||
{:else}
|
||||
<TerminalPane />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<aside class="sidebar">
|
||||
<SessionList />
|
||||
</aside>
|
||||
<main class="workspace">
|
||||
<TilingGrid />
|
||||
</main>
|
||||
<StatusBar />
|
||||
<SettingsDialog open={settingsOpen} onClose={() => settingsOpen = false} />
|
||||
{/if}
|
||||
<ToastContainer />
|
||||
<SettingsDialog open={settingsOpen} onClose={() => settingsOpen = false} />
|
||||
|
||||
<style>
|
||||
.detached-pane {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
|
|
|
|||
41
v2/src/lib/adapters/ctx-bridge.ts
Normal file
41
v2/src/lib/adapters/ctx-bridge.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export interface CtxProject {
|
||||
name: string;
|
||||
description: string;
|
||||
work_dir: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CtxEntry {
|
||||
project: string;
|
||||
key: string;
|
||||
value: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CtxSummary {
|
||||
project: string;
|
||||
summary: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export async function ctxListProjects(): Promise<CtxProject[]> {
|
||||
return invoke('ctx_list_projects');
|
||||
}
|
||||
|
||||
export async function ctxGetContext(project: string): Promise<CtxEntry[]> {
|
||||
return invoke('ctx_get_context', { project });
|
||||
}
|
||||
|
||||
export async function ctxGetShared(): Promise<CtxEntry[]> {
|
||||
return invoke('ctx_get_shared');
|
||||
}
|
||||
|
||||
export async function ctxGetSummaries(project: string, limit: number = 5): Promise<CtxSummary[]> {
|
||||
return invoke('ctx_get_summaries', { project, limit });
|
||||
}
|
||||
|
||||
export async function ctxSearch(query: string): Promise<CtxEntry[]> {
|
||||
return invoke('ctx_search', { query });
|
||||
}
|
||||
26
v2/src/lib/adapters/ssh-bridge.ts
Normal file
26
v2/src/lib/adapters/ssh-bridge.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export interface SshSession {
|
||||
id: string;
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
key_file: string;
|
||||
folder: string;
|
||||
color: string;
|
||||
created_at: number;
|
||||
last_used_at: number;
|
||||
}
|
||||
|
||||
export async function listSshSessions(): Promise<SshSession[]> {
|
||||
return invoke('ssh_session_list');
|
||||
}
|
||||
|
||||
export async function saveSshSession(session: SshSession): Promise<void> {
|
||||
return invoke('ssh_session_save', { session });
|
||||
}
|
||||
|
||||
export async function deleteSshSession(id: string): Promise<void> {
|
||||
return invoke('ssh_session_delete', { id });
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Agent Dispatcher — connects sidecar bridge events to agent store
|
||||
// Single listener that routes sidecar messages to the correct agent session
|
||||
|
||||
import { onSidecarMessage, onSidecarExited, type SidecarMessage } from './adapters/agent-bridge';
|
||||
import { onSidecarMessage, onSidecarExited, restartAgent, type SidecarMessage } from './adapters/agent-bridge';
|
||||
import { adaptSDKMessage } from './adapters/sdk-messages';
|
||||
import type { InitContent, CostContent } from './adapters/sdk-messages';
|
||||
import {
|
||||
|
|
@ -19,6 +19,10 @@ let unlistenExit: (() => void) | null = null;
|
|||
|
||||
// Sidecar liveness — checked by UI components
|
||||
let sidecarAlive = true;
|
||||
|
||||
// Sidecar crash recovery state
|
||||
const MAX_RESTART_ATTEMPTS = 3;
|
||||
let restartAttempts = 0;
|
||||
export function isSidecarAlive(): boolean {
|
||||
return sidecarAlive;
|
||||
}
|
||||
|
|
@ -33,6 +37,11 @@ export async function startAgentDispatcher(): Promise<void> {
|
|||
|
||||
unlistenMsg = await onSidecarMessage((msg: SidecarMessage) => {
|
||||
sidecarAlive = true;
|
||||
// Reset restart counter on any successful message — sidecar recovered
|
||||
if (restartAttempts > 0) {
|
||||
notify('success', 'Sidecar recovered');
|
||||
restartAttempts = 0;
|
||||
}
|
||||
|
||||
const sessionId = msg.sessionId;
|
||||
if (!sessionId) return;
|
||||
|
|
@ -61,15 +70,33 @@ export async function startAgentDispatcher(): Promise<void> {
|
|||
}
|
||||
});
|
||||
|
||||
unlistenExit = await onSidecarExited(() => {
|
||||
unlistenExit = await onSidecarExited(async () => {
|
||||
sidecarAlive = false;
|
||||
notify('error', 'Sidecar process crashed — agent features unavailable');
|
||||
// Mark all running sessions as errored
|
||||
for (const session of getAgentSessions()) {
|
||||
if (session.status === 'running' || session.status === 'starting') {
|
||||
updateAgentStatus(session.id, 'error', 'Sidecar crashed');
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt auto-restart with exponential backoff
|
||||
if (restartAttempts < MAX_RESTART_ATTEMPTS) {
|
||||
restartAttempts++;
|
||||
const delayMs = 1000 * Math.pow(2, restartAttempts - 1); // 1s, 2s, 4s
|
||||
notify('warning', `Sidecar crashed, restarting (attempt ${restartAttempts}/${MAX_RESTART_ATTEMPTS})...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
try {
|
||||
await restartAgent();
|
||||
sidecarAlive = true;
|
||||
// Note: restartAttempts is reset when next sidecar message arrives
|
||||
} catch {
|
||||
if (restartAttempts >= MAX_RESTART_ATTEMPTS) {
|
||||
notify('error', `Sidecar restart failed after ${MAX_RESTART_ATTEMPTS} attempts`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notify('error', `Sidecar restart failed after ${MAX_RESTART_ATTEMPTS} attempts`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { marked, Renderer } from 'marked';
|
||||
import { queryAgent, stopAgent, isAgentReady, restartAgent } from '../../adapters/agent-bridge';
|
||||
import {
|
||||
getAgentSession,
|
||||
|
|
@ -9,6 +10,7 @@
|
|||
} from '../../stores/agents.svelte';
|
||||
import { isSidecarAlive, setSidecarAlive } from '../../agent-dispatcher';
|
||||
import AgentTree from './AgentTree.svelte';
|
||||
import { getHighlighter, highlightCode, escapeHtml } from '../../utils/highlight';
|
||||
import type {
|
||||
AgentMessage,
|
||||
TextContent,
|
||||
|
|
@ -36,7 +38,25 @@
|
|||
let showTree = $state(false);
|
||||
let hasToolCalls = $derived(session?.messages.some(m => m.type === 'tool_call') ?? false);
|
||||
|
||||
const mdRenderer = new Renderer();
|
||||
mdRenderer.code = function({ text, lang }: { text: string; lang?: string }) {
|
||||
if (lang) {
|
||||
const highlighted = highlightCode(text, lang);
|
||||
if (highlighted !== escapeHtml(text)) return highlighted;
|
||||
}
|
||||
return `<pre><code>${escapeHtml(text)}</code></pre>`;
|
||||
};
|
||||
|
||||
function renderMarkdown(source: string): string {
|
||||
try {
|
||||
return marked.parse(source, { renderer: mdRenderer, async: false }) as string;
|
||||
} catch {
|
||||
return escapeHtml(source);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await getHighlighter();
|
||||
if (initialPrompt) {
|
||||
await startQuery(initialPrompt);
|
||||
}
|
||||
|
|
@ -164,7 +184,7 @@
|
|||
<span class="model">{(msg.content as import('../../adapters/sdk-messages').InitContent).model}</span>
|
||||
</div>
|
||||
{:else if msg.type === 'text'}
|
||||
<div class="msg-text">{(msg.content as TextContent).text}</div>
|
||||
<div class="msg-text markdown-body">{@html renderMarkdown((msg.content as TextContent).text)}</div>
|
||||
{:else if msg.type === 'thinking'}
|
||||
<details class="msg-thinking">
|
||||
<summary>Thinking...</summary>
|
||||
|
|
@ -217,6 +237,9 @@
|
|||
<span class="cost">${session.costUsd.toFixed(4)}</span>
|
||||
<span class="tokens">{session.inputTokens + session.outputTokens} tokens</span>
|
||||
<span class="duration">{(session.durationMs / 1000).toFixed(1)}s</span>
|
||||
{#if !autoScroll}
|
||||
<button class="scroll-btn" onclick={() => { autoScroll = true; scrollToBottom(); }}>Scroll to bottom</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if session.status === 'error'}
|
||||
<div class="error-bar">
|
||||
|
|
@ -334,11 +357,118 @@
|
|||
}
|
||||
|
||||
.msg-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(h1) {
|
||||
font-size: 1.4em;
|
||||
font-weight: 700;
|
||||
margin: 0.6em 0 0.3em;
|
||||
color: var(--ctp-lavender);
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(h2) {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
margin: 0.5em 0 0.3em;
|
||||
color: var(--ctp-blue);
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(h3) {
|
||||
font-size: 1.05em;
|
||||
font-weight: 600;
|
||||
margin: 0.4em 0 0.2em;
|
||||
color: var(--ctp-sapphire);
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(p) {
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(code) {
|
||||
background: var(--bg-surface);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
color: var(--ctp-green);
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(pre) {
|
||||
background: var(--bg-surface);
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--border-radius);
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(pre code) {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(.shiki) {
|
||||
background: var(--bg-surface) !important;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--border-radius);
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(.shiki code) {
|
||||
background: none !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(blockquote) {
|
||||
border-left: 3px solid var(--ctp-mauve);
|
||||
margin: 0.4em 0;
|
||||
padding: 2px 10px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(ul), .msg-text.markdown-body :global(ol) {
|
||||
padding-left: 20px;
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(li) {
|
||||
margin: 0.15em 0;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(a) {
|
||||
color: var(--ctp-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(table) {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0.4em 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(th), .msg-text.markdown-body :global(td) {
|
||||
border: 1px solid var(--border);
|
||||
padding: 4px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.msg-text.markdown-body :global(th) {
|
||||
background: var(--bg-surface);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.msg-thinking {
|
||||
color: var(--ctp-overlay1);
|
||||
font-size: 12px;
|
||||
|
|
|
|||
340
v2/src/lib/components/Context/ContextPane.svelte
Normal file
340
v2/src/lib/components/Context/ContextPane.svelte
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
ctxListProjects,
|
||||
ctxGetContext,
|
||||
ctxGetShared,
|
||||
ctxGetSummaries,
|
||||
ctxSearch,
|
||||
type CtxProject,
|
||||
type CtxEntry,
|
||||
type CtxSummary,
|
||||
} from '../../adapters/ctx-bridge';
|
||||
|
||||
interface Props {
|
||||
onExit?: () => void;
|
||||
}
|
||||
|
||||
let { onExit }: Props = $props();
|
||||
|
||||
let projects = $state<CtxProject[]>([]);
|
||||
let selectedProject = $state<string | null>(null);
|
||||
let entries = $state<CtxEntry[]>([]);
|
||||
let sharedEntries = $state<CtxEntry[]>([]);
|
||||
let summaries = $state<CtxSummary[]>([]);
|
||||
let searchQuery = $state('');
|
||||
let searchResults = $state<CtxEntry[]>([]);
|
||||
let error = $state('');
|
||||
let loading = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
projects = await ctxListProjects();
|
||||
sharedEntries = await ctxGetShared();
|
||||
} catch (e) {
|
||||
error = `ctx database not available: ${e}`;
|
||||
}
|
||||
});
|
||||
|
||||
async function selectProject(name: string) {
|
||||
selectedProject = name;
|
||||
loading = true;
|
||||
try {
|
||||
[entries, summaries] = await Promise.all([
|
||||
ctxGetContext(name),
|
||||
ctxGetSummaries(name, 5),
|
||||
]);
|
||||
error = '';
|
||||
} catch (e) {
|
||||
error = `Failed to load context: ${e}`;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
if (!searchQuery.trim()) {
|
||||
searchResults = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
searchResults = await ctxSearch(searchQuery);
|
||||
} catch (e) {
|
||||
error = `Search failed: ${e}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="context-pane">
|
||||
<div class="ctx-header">
|
||||
<h3>Context Manager</h3>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="Search contexts..."
|
||||
bind:value={searchQuery}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') handleSearch(); }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="ctx-error">{error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="ctx-body">
|
||||
{#if searchResults.length > 0}
|
||||
<div class="section">
|
||||
<h4>Search Results</h4>
|
||||
{#each searchResults as result}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-project">{result.project}</span>
|
||||
<span class="entry-key">{result.key}</span>
|
||||
</div>
|
||||
<pre class="entry-value">{result.value}</pre>
|
||||
</div>
|
||||
{/each}
|
||||
<button class="clear-btn" onclick={() => { searchResults = []; searchQuery = ''; }}>Clear search</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="project-list">
|
||||
<h4>Projects</h4>
|
||||
{#if projects.length === 0}
|
||||
<p class="empty">No projects registered. Use <code>ctx init</code> to add one.</p>
|
||||
{/if}
|
||||
{#each projects as project}
|
||||
<button
|
||||
class="project-btn"
|
||||
class:active={selectedProject === project.name}
|
||||
onclick={() => selectProject(project.name)}
|
||||
>
|
||||
<span class="project-name">{project.name}</span>
|
||||
<span class="project-desc">{project.description}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if sharedEntries.length > 0}
|
||||
<div class="section">
|
||||
<h4>Shared Context</h4>
|
||||
{#each sharedEntries as entry}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-key">{entry.key}</span>
|
||||
</div>
|
||||
<pre class="entry-value">{entry.value}</pre>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedProject && !loading}
|
||||
<div class="section">
|
||||
<h4>{selectedProject} Context</h4>
|
||||
{#if entries.length === 0}
|
||||
<p class="empty">No context entries for this project.</p>
|
||||
{/if}
|
||||
{#each entries as entry}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-key">{entry.key}</span>
|
||||
<span class="entry-date">{entry.updated_at}</span>
|
||||
</div>
|
||||
<pre class="entry-value">{entry.value}</pre>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if summaries.length > 0}
|
||||
<div class="section">
|
||||
<h4>Recent Sessions</h4>
|
||||
{#each summaries as summary}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-date">{summary.created_at}</span>
|
||||
</div>
|
||||
<pre class="entry-value">{summary.summary}</pre>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Loading...</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.context-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ctx-header {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ctx-header h3 {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ctx-error {
|
||||
color: var(--ctp-red);
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ctx-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.project-list {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-mauve);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.project-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.project-btn:hover { border-color: var(--accent); }
|
||||
.project-btn.active {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--bg-surface));
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.project-desc {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.entry {
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.entry-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.entry-project {
|
||||
font-size: 10px;
|
||||
color: var(--ctp-blue);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.entry-key {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-green);
|
||||
}
|
||||
|
||||
.entry-date {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.entry-value {
|
||||
font-size: 11px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--text-secondary);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.clear-btn:hover { color: var(--text-primary); }
|
||||
|
||||
.loading {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,10 +5,11 @@
|
|||
title: string;
|
||||
status?: 'idle' | 'running' | 'error' | 'done';
|
||||
onClose?: () => void;
|
||||
onDetach?: () => void;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { title, status = 'idle', onClose, children }: Props = $props();
|
||||
let { title, status = 'idle', onClose, onDetach, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="pane-container">
|
||||
|
|
@ -18,6 +19,9 @@
|
|||
{#if status !== 'idle'}
|
||||
<span class="status {status}">{status}</span>
|
||||
{/if}
|
||||
{#if onDetach}
|
||||
<button class="detach-btn" onclick={onDetach} title="Pop out to new window">↗</button>
|
||||
{/if}
|
||||
{#if onClose}
|
||||
<button class="close-btn" onclick={onClose} title="Close pane">×</button>
|
||||
{/if}
|
||||
|
|
@ -75,6 +79,18 @@
|
|||
.status.error { color: var(--ctp-red); }
|
||||
.status.done { color: var(--ctp-green); }
|
||||
|
||||
.detach-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.detach-btn:hover { color: var(--ctp-blue); }
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import TerminalPane from '../Terminal/TerminalPane.svelte';
|
||||
import AgentPane from '../Agent/AgentPane.svelte';
|
||||
import MarkdownPane from '../Markdown/MarkdownPane.svelte';
|
||||
import ContextPane from '../Context/ContextPane.svelte';
|
||||
import {
|
||||
getPanes,
|
||||
getGridTemplate,
|
||||
|
|
@ -10,9 +11,17 @@
|
|||
focusPane,
|
||||
removePane,
|
||||
} from '../../stores/layout.svelte';
|
||||
import { detachPane } from '../../utils/detach';
|
||||
import { isDetachedMode } from '../../utils/detach';
|
||||
|
||||
let gridTemplate = $derived(getGridTemplate());
|
||||
let panes = $derived(getPanes());
|
||||
let detached = isDetachedMode();
|
||||
|
||||
function handleDetach(pane: typeof panes[0]) {
|
||||
detachPane(pane);
|
||||
removePane(pane.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
@ -41,6 +50,7 @@
|
|||
title={pane.title}
|
||||
status={pane.focused ? 'running' : 'idle'}
|
||||
onClose={() => removePane(pane.id)}
|
||||
onDetach={detached ? undefined : () => handleDetach(pane)}
|
||||
>
|
||||
{#if pane.type === 'terminal'}
|
||||
<TerminalPane
|
||||
|
|
@ -55,6 +65,15 @@
|
|||
cwd={pane.cwd}
|
||||
onExit={() => removePane(pane.id)}
|
||||
/>
|
||||
{:else if pane.type === 'ssh'}
|
||||
<TerminalPane
|
||||
shell={pane.shell}
|
||||
cwd={pane.cwd}
|
||||
args={pane.args}
|
||||
onExit={() => removePane(pane.id)}
|
||||
/>
|
||||
{:else if pane.type === 'context'}
|
||||
<ContextPane onExit={() => removePane(pane.id)} />
|
||||
{:else if pane.type === 'markdown'}
|
||||
<MarkdownPane
|
||||
paneId={pane.id}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { marked } from 'marked';
|
||||
import { marked, Renderer } from 'marked';
|
||||
import { watchFile, unwatchFile, onFileChanged, type FileChangedPayload } from '../../adapters/file-bridge';
|
||||
import { getHighlighter, highlightCode, escapeHtml } from '../../utils/highlight';
|
||||
|
||||
interface Props {
|
||||
filePath: string;
|
||||
|
|
@ -15,9 +16,18 @@
|
|||
let error = $state('');
|
||||
let unlisten: (() => void) | undefined;
|
||||
|
||||
const renderer = new Renderer();
|
||||
renderer.code = function({ text, lang }: { text: string; lang?: string }) {
|
||||
if (lang) {
|
||||
const highlighted = highlightCode(text, lang);
|
||||
if (highlighted !== escapeHtml(text)) return highlighted;
|
||||
}
|
||||
return `<pre><code>${escapeHtml(text)}</code></pre>`;
|
||||
};
|
||||
|
||||
function renderMarkdown(source: string): void {
|
||||
try {
|
||||
renderedHtml = marked.parse(source, { async: false }) as string;
|
||||
renderedHtml = marked.parse(source, { renderer, async: false }) as string;
|
||||
error = '';
|
||||
} catch (e) {
|
||||
error = `Render error: ${e}`;
|
||||
|
|
@ -26,6 +36,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await getHighlighter();
|
||||
const content = await watchFile(paneId, filePath);
|
||||
renderMarkdown(content);
|
||||
|
||||
|
|
@ -125,6 +136,21 @@
|
|||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.markdown-body :global(.shiki) {
|
||||
background: var(--bg-surface) !important;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--border-radius);
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.markdown-body :global(.shiki code) {
|
||||
background: none !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body :global(blockquote) {
|
||||
border-left: 3px solid var(--ctp-mauve);
|
||||
margin: 0.5em 0;
|
||||
|
|
|
|||
281
v2/src/lib/components/SSH/SshDialog.svelte
Normal file
281
v2/src/lib/components/SSH/SshDialog.svelte
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
<script lang="ts">
|
||||
import { notify } from '../../stores/notifications.svelte';
|
||||
import { saveSshSession, type SshSession } from '../../adapters/ssh-bridge';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
editSession?: SshSession;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
}
|
||||
|
||||
let { open, editSession, onClose, onSaved }: Props = $props();
|
||||
|
||||
let name = $state('');
|
||||
let host = $state('');
|
||||
let port = $state(22);
|
||||
let username = $state('');
|
||||
let keyFile = $state('');
|
||||
let folder = $state('');
|
||||
|
||||
let validationError = $state('');
|
||||
|
||||
$effect(() => {
|
||||
if (open && editSession) {
|
||||
name = editSession.name;
|
||||
host = editSession.host;
|
||||
port = editSession.port;
|
||||
username = editSession.username;
|
||||
keyFile = editSession.key_file;
|
||||
folder = editSession.folder;
|
||||
} else if (open) {
|
||||
name = '';
|
||||
host = '';
|
||||
port = 22;
|
||||
username = '';
|
||||
keyFile = '';
|
||||
folder = '';
|
||||
}
|
||||
validationError = '';
|
||||
});
|
||||
|
||||
function validate(): boolean {
|
||||
if (!name.trim()) {
|
||||
validationError = 'Name is required';
|
||||
return false;
|
||||
}
|
||||
if (!host.trim()) {
|
||||
validationError = 'Host is required';
|
||||
return false;
|
||||
}
|
||||
if (!username.trim()) {
|
||||
validationError = 'Username is required';
|
||||
return false;
|
||||
}
|
||||
if (port < 1 || port > 65535) {
|
||||
validationError = 'Port must be between 1 and 65535';
|
||||
return false;
|
||||
}
|
||||
validationError = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!validate()) return;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const session: SshSession = {
|
||||
id: editSession?.id ?? crypto.randomUUID(),
|
||||
name: name.trim(),
|
||||
host: host.trim(),
|
||||
port,
|
||||
username: username.trim(),
|
||||
key_file: keyFile.trim(),
|
||||
folder: folder.trim(),
|
||||
color: editSession?.color ?? '#89b4fa',
|
||||
created_at: editSession?.created_at ?? now,
|
||||
last_used_at: now,
|
||||
};
|
||||
|
||||
try {
|
||||
await saveSshSession(session);
|
||||
notify('success', `SSH session "${session.name}" saved`);
|
||||
onSaved();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
notify('error', `Failed to save SSH session: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') onClose();
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) handleSave();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="overlay" onkeydown={handleKeydown}>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="backdrop" onclick={onClose}></div>
|
||||
<div class="dialog" role="dialog" aria-label="SSH Session">
|
||||
<div class="dialog-header">
|
||||
<h2>{editSession ? 'Edit' : 'New'} SSH Session</h2>
|
||||
<button class="close-btn" onclick={onClose}>×</button>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
{#if validationError}
|
||||
<div class="validation-error">{validationError}</div>
|
||||
{/if}
|
||||
<label class="field">
|
||||
<span class="field-label">Name</span>
|
||||
<input type="text" bind:value={name} placeholder="My Server" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Host</span>
|
||||
<input type="text" bind:value={host} placeholder="192.168.1.100 or server.example.com" />
|
||||
</label>
|
||||
<div class="field-row">
|
||||
<label class="field" style="flex: 1;">
|
||||
<span class="field-label">Username</span>
|
||||
<input type="text" bind:value={username} placeholder="root" />
|
||||
</label>
|
||||
<label class="field" style="width: 100px;">
|
||||
<span class="field-label">Port</span>
|
||||
<input type="number" bind:value={port} min="1" max="65535" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span class="field-label">SSH Key (optional)</span>
|
||||
<input type="text" bind:value={keyFile} placeholder="~/.ssh/id_ed25519" />
|
||||
<span class="field-hint">Leave empty to use default key or password auth</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Folder (optional)</span>
|
||||
<input type="text" bind:value={folder} placeholder="Group name for organizing" />
|
||||
<span class="field-hint">Sessions with the same folder are grouped together</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<button class="btn-cancel" onclick={onClose}>Cancel</button>
|
||||
<button class="btn-save" onclick={handleSave}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
position: relative;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
width: 420px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.dialog-header h2 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-btn:hover { color: var(--text-primary); }
|
||||
|
||||
.dialog-body {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.validation-error {
|
||||
background: rgba(243, 139, 168, 0.1);
|
||||
border: 1px solid var(--ctp-red);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--ctp-red);
|
||||
font-size: 11px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.field input {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-cancel, .btn-save {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 6px 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-cancel:hover { color: var(--text-primary); }
|
||||
|
||||
.btn-save {
|
||||
background: var(--accent);
|
||||
color: var(--ctp-crust);
|
||||
}
|
||||
|
||||
.btn-save:hover { opacity: 0.9; }
|
||||
</style>
|
||||
263
v2/src/lib/components/SSH/SshSessionList.svelte
Normal file
263
v2/src/lib/components/SSH/SshSessionList.svelte
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { listSshSessions, deleteSshSession, type SshSession } from '../../adapters/ssh-bridge';
|
||||
import { addPane } from '../../stores/layout.svelte';
|
||||
import { notify } from '../../stores/notifications.svelte';
|
||||
import SshDialog from './SshDialog.svelte';
|
||||
|
||||
let sessions = $state<SshSession[]>([]);
|
||||
let dialogOpen = $state(false);
|
||||
let editingSession = $state<SshSession | undefined>(undefined);
|
||||
|
||||
onMount(() => {
|
||||
loadSessions();
|
||||
});
|
||||
|
||||
async function loadSessions() {
|
||||
try {
|
||||
sessions = await listSshSessions();
|
||||
} catch (e) {
|
||||
console.warn('Failed to load SSH sessions:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function openNewDialog() {
|
||||
editingSession = undefined;
|
||||
dialogOpen = true;
|
||||
}
|
||||
|
||||
function openEditDialog(session: SshSession) {
|
||||
editingSession = session;
|
||||
dialogOpen = true;
|
||||
}
|
||||
|
||||
function connectSsh(session: SshSession) {
|
||||
const id = crypto.randomUUID();
|
||||
const args: string[] = [];
|
||||
|
||||
// Build ssh command arguments
|
||||
args.push('-p', String(session.port));
|
||||
if (session.key_file) {
|
||||
args.push('-i', session.key_file);
|
||||
}
|
||||
args.push(`${session.username}@${session.host}`);
|
||||
|
||||
addPane({
|
||||
id,
|
||||
type: 'ssh',
|
||||
title: `SSH: ${session.name}`,
|
||||
shell: '/usr/bin/ssh',
|
||||
args,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDelete(session: SshSession, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await deleteSshSession(session.id);
|
||||
sessions = sessions.filter(s => s.id !== session.id);
|
||||
notify('success', `Deleted SSH session "${session.name}"`);
|
||||
} catch (e) {
|
||||
notify('error', `Failed to delete SSH session: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Group sessions by folder
|
||||
let grouped = $derived(() => {
|
||||
const groups = new Map<string, SshSession[]>();
|
||||
for (const s of sessions) {
|
||||
const folder = s.folder || '';
|
||||
if (!groups.has(folder)) groups.set(folder, []);
|
||||
groups.get(folder)!.push(s);
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="ssh-sessions">
|
||||
<div class="ssh-header">
|
||||
<h3>SSH</h3>
|
||||
<button class="new-btn" onclick={openNewDialog} title="Add SSH session">+</button>
|
||||
</div>
|
||||
|
||||
{#if sessions.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>No SSH sessions.</p>
|
||||
</div>
|
||||
{:else}
|
||||
{@const groups = grouped()}
|
||||
{#each [...groups.entries()] as [folder, folderSessions] (folder)}
|
||||
{#if folder}
|
||||
<div class="folder-label">{folder}</div>
|
||||
{/if}
|
||||
<ul class="ssh-list">
|
||||
{#each folderSessions as session (session.id)}
|
||||
<li class="ssh-item">
|
||||
<button class="ssh-btn" onclick={() => connectSsh(session)} title="Connect to {session.host}">
|
||||
<span class="ssh-color" style:background={session.color}></span>
|
||||
<span class="ssh-info">
|
||||
<span class="ssh-name">{session.name}</span>
|
||||
<span class="ssh-host">{session.username}@{session.host}:{session.port}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="edit-btn" onclick={() => openEditDialog(session)} title="Edit">E</button>
|
||||
<button class="remove-btn" onclick={(e) => handleDelete(session, e)} title="Delete">×</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<SshDialog
|
||||
open={dialogOpen}
|
||||
editSession={editingSession}
|
||||
onClose={() => { dialogOpen = false; }}
|
||||
onSaved={loadSessions}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.ssh-sessions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ssh-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ssh-header h3 {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.new-btn {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.new-btn:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.folder-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-overlay0);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 4px 0 2px;
|
||||
}
|
||||
|
||||
.ssh-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.ssh-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.ssh-item:hover {
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.ssh-btn {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ssh-btn:hover { color: var(--text-primary); }
|
||||
|
||||
.ssh-color {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ssh-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ssh-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ssh-host {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 2px 4px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ssh-item:hover .edit-btn { opacity: 1; }
|
||||
.ssh-item:hover .remove-btn { opacity: 1; }
|
||||
.edit-btn:hover { color: var(--ctp-yellow); }
|
||||
.remove-btn:hover { color: var(--ctp-red); }
|
||||
</style>
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { getSetting, setSetting } from '../../adapters/settings-bridge';
|
||||
import { notify } from '../../stores/notifications.svelte';
|
||||
import { getCurrentFlavor, setFlavor } from '../../stores/theme.svelte';
|
||||
import { ALL_FLAVORS, FLAVOR_LABELS, type CatppuccinFlavor } from '../../styles/themes';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -13,12 +15,14 @@
|
|||
let defaultShell = $state('');
|
||||
let defaultCwd = $state('');
|
||||
let maxPanes = $state('4');
|
||||
let themeFlavor = $state<CatppuccinFlavor>('mocha');
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
defaultShell = (await getSetting('default_shell')) ?? '';
|
||||
defaultCwd = (await getSetting('default_cwd')) ?? '';
|
||||
maxPanes = (await getSetting('max_panes')) ?? '4';
|
||||
themeFlavor = getCurrentFlavor();
|
||||
} catch {
|
||||
// Use defaults
|
||||
}
|
||||
|
|
@ -29,6 +33,7 @@
|
|||
if (defaultShell) await setSetting('default_shell', defaultShell);
|
||||
if (defaultCwd) await setSetting('default_cwd', defaultCwd);
|
||||
await setSetting('max_panes', maxPanes);
|
||||
await setFlavor(themeFlavor);
|
||||
notify('success', 'Settings saved');
|
||||
onClose();
|
||||
} catch (e) {
|
||||
|
|
@ -67,6 +72,15 @@
|
|||
<input type="number" bind:value={maxPanes} min="1" max="8" />
|
||||
<span class="field-hint">Maximum simultaneous panes (1-8)</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Theme</span>
|
||||
<select bind:value={themeFlavor}>
|
||||
{#each ALL_FLAVORS as flavor}
|
||||
<option value={flavor}>{FLAVOR_LABELS[flavor]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="field-hint">Catppuccin color scheme. New terminals use the updated theme.</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<button class="btn-cancel" onclick={onClose}>Cancel</button>
|
||||
|
|
@ -146,7 +160,7 @@
|
|||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.field input {
|
||||
.field input, .field select {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
|
|
@ -156,7 +170,7 @@
|
|||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.field input:focus {
|
||||
.field input:focus, .field select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
setPreset,
|
||||
type LayoutPreset,
|
||||
} from '../../stores/layout.svelte';
|
||||
import SshSessionList from '../SSH/SshSessionList.svelte';
|
||||
|
||||
let panes = $derived(getPanes());
|
||||
let preset = $derived(getActivePreset());
|
||||
|
|
@ -34,6 +35,20 @@
|
|||
});
|
||||
}
|
||||
|
||||
function openContext() {
|
||||
const existing = panes.find(p => p.type === 'context');
|
||||
if (existing) {
|
||||
focusPane(existing.id);
|
||||
return;
|
||||
}
|
||||
const id = crypto.randomUUID();
|
||||
addPane({
|
||||
id,
|
||||
type: 'context',
|
||||
title: 'Context',
|
||||
});
|
||||
}
|
||||
|
||||
let fileInputEl: HTMLInputElement | undefined = $state();
|
||||
|
||||
function openMarkdown() {
|
||||
|
|
@ -62,6 +77,7 @@
|
|||
<div class="header">
|
||||
<h2>Sessions</h2>
|
||||
<div class="header-buttons">
|
||||
<button class="new-btn" onclick={openContext} title="Context manager">C</button>
|
||||
<button class="new-btn" onclick={openMarkdown} title="Open markdown file">M</button>
|
||||
<button class="new-btn" onclick={newAgent} title="New agent (Ctrl+Shift+N)">A</button>
|
||||
<button class="new-btn" onclick={newTerminal} title="New terminal (Ctrl+N)">+</button>
|
||||
|
|
@ -96,7 +112,7 @@
|
|||
{#each panes as pane (pane.id)}
|
||||
<li class="pane-item" class:focused={pane.focused}>
|
||||
<button class="pane-btn" onclick={() => focusPane(pane.id)}>
|
||||
<span class="pane-icon">{pane.type === 'terminal' ? '>' : pane.type === 'agent' ? '*' : pane.type === 'markdown' ? 'M' : '#'}</span>
|
||||
<span class="pane-icon">{pane.type === 'terminal' ? '>' : pane.type === 'agent' ? '*' : pane.type === 'markdown' ? 'M' : pane.type === 'ssh' ? '@' : pane.type === 'context' ? 'C' : '#'}</span>
|
||||
<span class="pane-name">{pane.title}</span>
|
||||
</button>
|
||||
<button class="remove-btn" onclick={() => removePane(pane.id)}>×</button>
|
||||
|
|
@ -104,9 +120,18 @@
|
|||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<div class="divider"></div>
|
||||
<SshSessionList />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { CanvasAddon } from '@xterm/addon-canvas';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { spawnPty, writePty, resizePty, killPty, onPtyData, onPtyExit } from '../../adapters/pty-bridge';
|
||||
import { getXtermTheme } from '../../stores/theme.svelte';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
|
||||
|
|
@ -24,35 +25,9 @@
|
|||
let unlistenExit: UnlistenFn | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
// Catppuccin Mocha xterm theme
|
||||
const catppuccinTheme = {
|
||||
background: '#1e1e2e',
|
||||
foreground: '#cdd6f4',
|
||||
cursor: '#f5e0dc',
|
||||
cursorAccent: '#1e1e2e',
|
||||
selectionBackground: '#45475a',
|
||||
selectionForeground: '#cdd6f4',
|
||||
black: '#45475a',
|
||||
red: '#f38ba8',
|
||||
green: '#a6e3a1',
|
||||
yellow: '#f9e2af',
|
||||
blue: '#89b4fa',
|
||||
magenta: '#f5c2e7',
|
||||
cyan: '#94e2d5',
|
||||
white: '#bac2de',
|
||||
brightBlack: '#585b70',
|
||||
brightRed: '#f38ba8',
|
||||
brightGreen: '#a6e3a1',
|
||||
brightYellow: '#f9e2af',
|
||||
brightBlue: '#89b4fa',
|
||||
brightMagenta: '#f5c2e7',
|
||||
brightCyan: '#94e2d5',
|
||||
brightWhite: '#a6adc8',
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
term = new Terminal({
|
||||
theme: catppuccinTheme,
|
||||
theme: getXtermTheme(),
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.2,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
|
||||
export type LayoutPreset = '1-col' | '2-col' | '3-col' | '2x2' | 'master-stack';
|
||||
|
||||
export type PaneType = 'terminal' | 'agent' | 'markdown' | 'empty';
|
||||
export type PaneType = 'terminal' | 'agent' | 'markdown' | 'ssh' | 'context' | 'empty';
|
||||
|
||||
export interface Pane {
|
||||
id: string;
|
||||
|
|
|
|||
47
v2/src/lib/stores/theme.svelte.ts
Normal file
47
v2/src/lib/stores/theme.svelte.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Theme store — persists Catppuccin flavor selection via settings bridge
|
||||
|
||||
import { getSetting, setSetting } from '../adapters/settings-bridge';
|
||||
import {
|
||||
type CatppuccinFlavor,
|
||||
buildXtermTheme,
|
||||
applyCssVariables,
|
||||
type XtermTheme,
|
||||
} from '../styles/themes';
|
||||
|
||||
let currentFlavor = $state<CatppuccinFlavor>('mocha');
|
||||
|
||||
export function getCurrentFlavor(): CatppuccinFlavor {
|
||||
return currentFlavor;
|
||||
}
|
||||
|
||||
export function getXtermTheme(): XtermTheme {
|
||||
return buildXtermTheme(currentFlavor);
|
||||
}
|
||||
|
||||
/** Change flavor, apply CSS variables, and persist to settings DB */
|
||||
export async function setFlavor(flavor: CatppuccinFlavor): Promise<void> {
|
||||
currentFlavor = flavor;
|
||||
applyCssVariables(flavor);
|
||||
try {
|
||||
await setSetting('theme', flavor);
|
||||
} catch (e) {
|
||||
console.error('Failed to persist theme setting:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Load saved flavor from settings DB and apply. Call once on app startup. */
|
||||
export async function initTheme(): Promise<void> {
|
||||
try {
|
||||
const saved = await getSetting('theme');
|
||||
if (saved && ['latte', 'frappe', 'macchiato', 'mocha'].includes(saved)) {
|
||||
currentFlavor = saved as CatppuccinFlavor;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to default (mocha) — catppuccin.css provides Mocha defaults
|
||||
}
|
||||
// Always apply to sync CSS vars with current flavor
|
||||
// (skip if mocha — catppuccin.css already has Mocha values)
|
||||
if (currentFlavor !== 'mocha') {
|
||||
applyCssVariables(currentFlavor);
|
||||
}
|
||||
}
|
||||
254
v2/src/lib/styles/themes.ts
Normal file
254
v2/src/lib/styles/themes.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
// Catppuccin theme flavors — https://catppuccin.com/palette
|
||||
// Each flavor provides CSS custom properties and an xterm.js theme object.
|
||||
|
||||
export type CatppuccinFlavor = 'latte' | 'frappe' | 'macchiato' | 'mocha';
|
||||
|
||||
export interface CatppuccinPalette {
|
||||
rosewater: string;
|
||||
flamingo: string;
|
||||
pink: string;
|
||||
mauve: string;
|
||||
red: string;
|
||||
maroon: string;
|
||||
peach: string;
|
||||
yellow: string;
|
||||
green: string;
|
||||
teal: string;
|
||||
sky: string;
|
||||
sapphire: string;
|
||||
blue: string;
|
||||
lavender: string;
|
||||
text: string;
|
||||
subtext1: string;
|
||||
subtext0: string;
|
||||
overlay2: string;
|
||||
overlay1: string;
|
||||
overlay0: string;
|
||||
surface2: string;
|
||||
surface1: string;
|
||||
surface0: string;
|
||||
base: string;
|
||||
mantle: string;
|
||||
crust: string;
|
||||
}
|
||||
|
||||
export interface XtermTheme {
|
||||
background: string;
|
||||
foreground: string;
|
||||
cursor: string;
|
||||
cursorAccent: string;
|
||||
selectionBackground: string;
|
||||
selectionForeground: string;
|
||||
black: string;
|
||||
red: string;
|
||||
green: string;
|
||||
yellow: string;
|
||||
blue: string;
|
||||
magenta: string;
|
||||
cyan: string;
|
||||
white: string;
|
||||
brightBlack: string;
|
||||
brightRed: string;
|
||||
brightGreen: string;
|
||||
brightYellow: string;
|
||||
brightBlue: string;
|
||||
brightMagenta: string;
|
||||
brightCyan: string;
|
||||
brightWhite: string;
|
||||
}
|
||||
|
||||
const palettes: Record<CatppuccinFlavor, CatppuccinPalette> = {
|
||||
latte: {
|
||||
rosewater: '#dc8a78',
|
||||
flamingo: '#dd7878',
|
||||
pink: '#ea76cb',
|
||||
mauve: '#8839ef',
|
||||
red: '#d20f39',
|
||||
maroon: '#e64553',
|
||||
peach: '#fe640b',
|
||||
yellow: '#df8e1d',
|
||||
green: '#40a02b',
|
||||
teal: '#179299',
|
||||
sky: '#04a5e5',
|
||||
sapphire: '#209fb5',
|
||||
blue: '#1e66f5',
|
||||
lavender: '#7287fd',
|
||||
text: '#4c4f69',
|
||||
subtext1: '#5c5f77',
|
||||
subtext0: '#6c6f85',
|
||||
overlay2: '#7c7f93',
|
||||
overlay1: '#8c8fa1',
|
||||
overlay0: '#9ca0b0',
|
||||
surface2: '#acb0be',
|
||||
surface1: '#bcc0cc',
|
||||
surface0: '#ccd0da',
|
||||
base: '#eff1f5',
|
||||
mantle: '#e6e9ef',
|
||||
crust: '#dce0e8',
|
||||
},
|
||||
frappe: {
|
||||
rosewater: '#f2d5cf',
|
||||
flamingo: '#eebebe',
|
||||
pink: '#f4b8e4',
|
||||
mauve: '#ca9ee6',
|
||||
red: '#e78284',
|
||||
maroon: '#ea999c',
|
||||
peach: '#ef9f76',
|
||||
yellow: '#e5c890',
|
||||
green: '#a6d189',
|
||||
teal: '#81c8be',
|
||||
sky: '#99d1db',
|
||||
sapphire: '#85c1dc',
|
||||
blue: '#8caaee',
|
||||
lavender: '#babbf1',
|
||||
text: '#c6d0f5',
|
||||
subtext1: '#b5bfe2',
|
||||
subtext0: '#a5adce',
|
||||
overlay2: '#949cbb',
|
||||
overlay1: '#838ba7',
|
||||
overlay0: '#737994',
|
||||
surface2: '#626880',
|
||||
surface1: '#51576d',
|
||||
surface0: '#414559',
|
||||
base: '#303446',
|
||||
mantle: '#292c3c',
|
||||
crust: '#232634',
|
||||
},
|
||||
macchiato: {
|
||||
rosewater: '#f4dbd6',
|
||||
flamingo: '#f0c6c6',
|
||||
pink: '#f5bde6',
|
||||
mauve: '#c6a0f6',
|
||||
red: '#ed8796',
|
||||
maroon: '#ee99a0',
|
||||
peach: '#f5a97f',
|
||||
yellow: '#eed49f',
|
||||
green: '#a6da95',
|
||||
teal: '#8bd5ca',
|
||||
sky: '#91d7e3',
|
||||
sapphire: '#7dc4e4',
|
||||
blue: '#8aadf4',
|
||||
lavender: '#b7bdf8',
|
||||
text: '#cad3f5',
|
||||
subtext1: '#b8c0e0',
|
||||
subtext0: '#a5adcb',
|
||||
overlay2: '#939ab7',
|
||||
overlay1: '#8087a2',
|
||||
overlay0: '#6e738d',
|
||||
surface2: '#5b6078',
|
||||
surface1: '#494d64',
|
||||
surface0: '#363a4f',
|
||||
base: '#24273a',
|
||||
mantle: '#1e2030',
|
||||
crust: '#181926',
|
||||
},
|
||||
mocha: {
|
||||
rosewater: '#f5e0dc',
|
||||
flamingo: '#f2cdcd',
|
||||
pink: '#f5c2e7',
|
||||
mauve: '#cba6f7',
|
||||
red: '#f38ba8',
|
||||
maroon: '#eba0ac',
|
||||
peach: '#fab387',
|
||||
yellow: '#f9e2af',
|
||||
green: '#a6e3a1',
|
||||
teal: '#94e2d5',
|
||||
sky: '#89dceb',
|
||||
sapphire: '#74c7ec',
|
||||
blue: '#89b4fa',
|
||||
lavender: '#b4befe',
|
||||
text: '#cdd6f4',
|
||||
subtext1: '#bac2de',
|
||||
subtext0: '#a6adc8',
|
||||
overlay2: '#9399b2',
|
||||
overlay1: '#7f849c',
|
||||
overlay0: '#6c7086',
|
||||
surface2: '#585b70',
|
||||
surface1: '#45475a',
|
||||
surface0: '#313244',
|
||||
base: '#1e1e2e',
|
||||
mantle: '#181825',
|
||||
crust: '#11111b',
|
||||
},
|
||||
};
|
||||
|
||||
export function getPalette(flavor: CatppuccinFlavor): CatppuccinPalette {
|
||||
return palettes[flavor];
|
||||
}
|
||||
|
||||
/** Build xterm.js ITheme from a Catppuccin palette */
|
||||
export function buildXtermTheme(flavor: CatppuccinFlavor): XtermTheme {
|
||||
const p = palettes[flavor];
|
||||
return {
|
||||
background: p.base,
|
||||
foreground: p.text,
|
||||
cursor: p.rosewater,
|
||||
cursorAccent: p.base,
|
||||
selectionBackground: p.surface1,
|
||||
selectionForeground: p.text,
|
||||
black: p.surface1,
|
||||
red: p.red,
|
||||
green: p.green,
|
||||
yellow: p.yellow,
|
||||
blue: p.blue,
|
||||
magenta: p.pink,
|
||||
cyan: p.teal,
|
||||
white: p.subtext1,
|
||||
brightBlack: p.surface2,
|
||||
brightRed: p.red,
|
||||
brightGreen: p.green,
|
||||
brightYellow: p.yellow,
|
||||
brightBlue: p.blue,
|
||||
brightMagenta: p.pink,
|
||||
brightCyan: p.teal,
|
||||
brightWhite: p.subtext0,
|
||||
};
|
||||
}
|
||||
|
||||
/** CSS custom property names mapped to palette keys */
|
||||
const CSS_VAR_MAP: [string, keyof CatppuccinPalette][] = [
|
||||
['--ctp-rosewater', 'rosewater'],
|
||||
['--ctp-flamingo', 'flamingo'],
|
||||
['--ctp-pink', 'pink'],
|
||||
['--ctp-mauve', 'mauve'],
|
||||
['--ctp-red', 'red'],
|
||||
['--ctp-maroon', 'maroon'],
|
||||
['--ctp-peach', 'peach'],
|
||||
['--ctp-yellow', 'yellow'],
|
||||
['--ctp-green', 'green'],
|
||||
['--ctp-teal', 'teal'],
|
||||
['--ctp-sky', 'sky'],
|
||||
['--ctp-sapphire', 'sapphire'],
|
||||
['--ctp-blue', 'blue'],
|
||||
['--ctp-lavender', 'lavender'],
|
||||
['--ctp-text', 'text'],
|
||||
['--ctp-subtext1', 'subtext1'],
|
||||
['--ctp-subtext0', 'subtext0'],
|
||||
['--ctp-overlay2', 'overlay2'],
|
||||
['--ctp-overlay1', 'overlay1'],
|
||||
['--ctp-overlay0', 'overlay0'],
|
||||
['--ctp-surface2', 'surface2'],
|
||||
['--ctp-surface1', 'surface1'],
|
||||
['--ctp-surface0', 'surface0'],
|
||||
['--ctp-base', 'base'],
|
||||
['--ctp-mantle', 'mantle'],
|
||||
['--ctp-crust', 'crust'],
|
||||
];
|
||||
|
||||
/** Apply a Catppuccin flavor's CSS custom properties to document root */
|
||||
export function applyCssVariables(flavor: CatppuccinFlavor): void {
|
||||
const p = palettes[flavor];
|
||||
const style = document.documentElement.style;
|
||||
for (const [varName, key] of CSS_VAR_MAP) {
|
||||
style.setProperty(varName, p[key]);
|
||||
}
|
||||
}
|
||||
|
||||
export const FLAVOR_LABELS: Record<CatppuccinFlavor, string> = {
|
||||
latte: 'Latte (Light)',
|
||||
frappe: 'Frappe',
|
||||
macchiato: 'Macchiato',
|
||||
mocha: 'Mocha (Default)',
|
||||
};
|
||||
|
||||
export const ALL_FLAVORS: CatppuccinFlavor[] = ['latte', 'frappe', 'macchiato', 'mocha'];
|
||||
68
v2/src/lib/utils/detach.ts
Normal file
68
v2/src/lib/utils/detach.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Detachable pane support — opens panes in separate OS windows
|
||||
// Uses Tauri's WebviewWindow API
|
||||
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import type { Pane } from '../stores/layout.svelte';
|
||||
|
||||
let detachCounter = 0;
|
||||
|
||||
export async function detachPane(pane: Pane): Promise<void> {
|
||||
detachCounter++;
|
||||
const label = `detached-${detachCounter}`;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
detached: 'true',
|
||||
type: pane.type,
|
||||
title: pane.title,
|
||||
});
|
||||
|
||||
if (pane.shell) params.set('shell', pane.shell);
|
||||
if (pane.cwd) params.set('cwd', pane.cwd);
|
||||
if (pane.args) params.set('args', JSON.stringify(pane.args));
|
||||
if (pane.type === 'agent') params.set('sessionId', pane.id);
|
||||
|
||||
const webview = new WebviewWindow(label, {
|
||||
url: `index.html?${params.toString()}`,
|
||||
title: `BTerminal — ${pane.title}`,
|
||||
width: 800,
|
||||
height: 600,
|
||||
decorations: true,
|
||||
resizable: true,
|
||||
});
|
||||
|
||||
// Wait for the window to be created
|
||||
await webview.once('tauri://created', () => {
|
||||
// Window created successfully
|
||||
});
|
||||
|
||||
await webview.once('tauri://error', (e) => {
|
||||
console.error('Failed to create detached window:', e);
|
||||
});
|
||||
}
|
||||
|
||||
export function isDetachedMode(): boolean {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('detached') === 'true';
|
||||
}
|
||||
|
||||
export function getDetachedConfig(): {
|
||||
type: string;
|
||||
title: string;
|
||||
shell?: string;
|
||||
cwd?: string;
|
||||
args?: string[];
|
||||
sessionId?: string;
|
||||
} | null {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('detached') !== 'true') return null;
|
||||
|
||||
const argsStr = params.get('args');
|
||||
return {
|
||||
type: params.get('type') ?? 'terminal',
|
||||
title: params.get('title') ?? 'Detached',
|
||||
shell: params.get('shell') ?? undefined,
|
||||
cwd: params.get('cwd') ?? undefined,
|
||||
args: argsStr ? JSON.parse(argsStr) : undefined,
|
||||
sessionId: params.get('sessionId') ?? undefined,
|
||||
};
|
||||
}
|
||||
51
v2/src/lib/utils/highlight.ts
Normal file
51
v2/src/lib/utils/highlight.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { createHighlighter, type Highlighter } from 'shiki';
|
||||
|
||||
let highlighter: Highlighter | null = null;
|
||||
let initPromise: Promise<Highlighter> | null = null;
|
||||
|
||||
// Use catppuccin-mocha theme (bundled with shiki)
|
||||
const THEME = 'catppuccin-mocha';
|
||||
|
||||
// Common languages to preload
|
||||
const LANGS = [
|
||||
'typescript', 'javascript', 'rust', 'python', 'bash',
|
||||
'json', 'html', 'css', 'svelte', 'sql', 'yaml', 'toml', 'markdown',
|
||||
];
|
||||
|
||||
export async function getHighlighter(): Promise<Highlighter> {
|
||||
if (highlighter) return highlighter;
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
initPromise = createHighlighter({
|
||||
themes: [THEME],
|
||||
langs: LANGS,
|
||||
});
|
||||
|
||||
highlighter = await initPromise;
|
||||
return highlighter;
|
||||
}
|
||||
|
||||
export function highlightCode(code: string, lang: string): string {
|
||||
if (!highlighter) return escapeHtml(code);
|
||||
|
||||
try {
|
||||
const loadedLangs = highlighter.getLoadedLanguages();
|
||||
if (!loadedLangs.includes(lang as any)) {
|
||||
return escapeHtml(code);
|
||||
}
|
||||
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
theme: THEME,
|
||||
});
|
||||
} catch {
|
||||
return escapeHtml(code);
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
32
v2/src/lib/utils/updater.ts
Normal file
32
v2/src/lib/utils/updater.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Auto-update checker — uses Tauri updater plugin
|
||||
// Requires signing key to be configured in tauri.conf.json before use
|
||||
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
|
||||
export async function checkForUpdates(): Promise<{
|
||||
available: boolean;
|
||||
version?: string;
|
||||
notes?: string;
|
||||
}> {
|
||||
try {
|
||||
const update = await check();
|
||||
if (update) {
|
||||
return {
|
||||
available: true,
|
||||
version: update.version,
|
||||
notes: update.body ?? undefined,
|
||||
};
|
||||
}
|
||||
return { available: false };
|
||||
} catch {
|
||||
// Updater not configured or network error — silently skip
|
||||
return { available: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function installUpdate(): Promise<void> {
|
||||
const update = await check();
|
||||
if (update) {
|
||||
await update.downloadAndInstall();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue