Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a91e04ff9 | ||
|
|
76b1134c95 | ||
|
|
d98d519fd3 | ||
|
|
02407e0f7a | ||
|
|
0261154a5e | ||
|
|
d2b68159be | ||
|
|
aab0692403 | ||
|
|
17a3e43ac7 | ||
|
|
a2127a11ac | ||
|
|
ea4c687125 | ||
|
|
de20b3adf3 | ||
|
|
929e79befd | ||
|
|
3522d3dff5 | ||
|
|
1af01680ee | ||
|
|
67f5f830a3 | ||
|
|
81102cc6bf | ||
|
|
afa7243eab | ||
|
|
37b7c1e53c | ||
|
|
ba61ab79e2 | ||
|
|
37d075fbb3 | ||
|
|
2961d41be3 | ||
|
|
1bb5aedfdb | ||
|
|
0a793fb1c6 | ||
|
|
a401eeec11 | ||
|
|
d9bcc66930 | ||
|
|
01921e3454 |
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Neural Nomads
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
37
package-lock.json
generated
37
package-lock.json
generated
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
"google-auth-library": "^10.5.0"
|
"google-auth-library": "^10.5.0"
|
||||||
@@ -1419,6 +1420,16 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/api": {
|
||||||
|
"version": "2.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz",
|
||||||
|
"integrity": "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==",
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/tauri"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/cli": {
|
"node_modules/@tauri-apps/cli": {
|
||||||
"version": "2.9.4",
|
"version": "2.9.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -1462,6 +1473,15 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-opener": {
|
||||||
|
"version": "2.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz",
|
||||||
|
"integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tootallnate/once": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -7384,7 +7404,8 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
"@neuralnomads/codenomad": "file:../server"
|
"@neuralnomads/codenomad": "file:../server"
|
||||||
@@ -7418,7 +7439,8 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
@@ -7455,14 +7477,16 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
@@ -7471,6 +7495,7 @@
|
|||||||
"@suid/icons-material": "^0.9.0",
|
"@suid/icons-material": "^0.9.0",
|
||||||
"@suid/material": "^0.19.0",
|
"@suid/material": "^0.19.0",
|
||||||
"@suid/system": "^0.14.0",
|
"@suid/system": "^0.14.0",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||||
"ansi-sequence-parser": "^1.1.3",
|
"ansi-sequence-parser": "^1.1.3",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
|
"license": "MIT",
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/server",
|
"packages/server",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui-host-worker",
|
"name": "@codenomad/ui-host-worker",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:manifest": "node ./scripts/build-manifest.mjs",
|
"build:manifest": "node ./scripts/build-manifest.mjs",
|
||||||
|
|||||||
@@ -505,7 +505,6 @@ app.on("before-quit", async (event) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
if (process.platform !== "darwin") {
|
// CodeNomad supports a single window; closing it should quit the app on all platforms.
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Neural Nomads",
|
"name": "Neural Nomads",
|
||||||
"email": "codenomad@neuralnomads.ai"
|
"email": "codenomad@neuralnomads.ai"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"name": "@codenomad/opencode-config",
|
"name": "@codenomad/opencode-config",
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.1.36"
|
"@opencode-ai/plugin": "1.1.42"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,17 @@ You can configure the server using flags or environment variables:
|
|||||||
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
||||||
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
||||||
| `--log-level <level>` | `CLI_LOG_LEVEL` | Logging level (trace, debug, info, warn, error) |
|
| `--log-level <level>` | `CLI_LOG_LEVEL` | Logging level (trace, debug, info, warn, error) |
|
||||||
|
| `--username <username>` | `CODENOMAD_SERVER_USERNAME` | Username for CodeNomad's internal auth (default `codenomad`) |
|
||||||
|
| `--password <password>` | `CODENOMAD_SERVER_PASSWORD` | Password for CodeNomad's internal auth |
|
||||||
|
| `--generate-token` | `CODENOMAD_GENERATE_TOKEN` | Emit a one-time local bootstrap token for desktop flows |
|
||||||
|
| `--dangerously-skip-auth` | `CODENOMAD_SKIP_AUTH` | Disable CodeNomad's internal auth (use only behind a trusted perimeter) |
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
|
||||||
|
- `--dangerously-skip-auth` / `CODENOMAD_SKIP_AUTH=true` disables the login prompt and treats all requests as authenticated.
|
||||||
|
Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.).
|
||||||
|
If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API.
|
||||||
|
|
||||||
### Data Storage
|
### Data Storage
|
||||||
- **Config**: `~/.config/codenomad/config.json`
|
- **Config**: `~/.config/codenomad/config.json`
|
||||||
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
||||||
|
|
||||||
|
|||||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Neural Nomads",
|
"name": "Neural Nomads",
|
||||||
"email": "codenomad@neuralnomads.ai"
|
"email": "codenomad@neuralnomads.ai"
|
||||||
|
|||||||
@@ -15,15 +15,25 @@ export interface AuthManagerInit {
|
|||||||
username: string
|
username: string
|
||||||
password?: string
|
password?: string
|
||||||
generateToken: boolean
|
generateToken: boolean
|
||||||
|
dangerouslySkipAuth?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthManager {
|
export class AuthManager {
|
||||||
private readonly authStore: AuthStore
|
private readonly authStore: AuthStore | null
|
||||||
private readonly tokenManager: TokenManager | null
|
private readonly tokenManager: TokenManager | null
|
||||||
private readonly sessionManager = new SessionManager()
|
private readonly sessionManager = new SessionManager()
|
||||||
private readonly cookieName = DEFAULT_AUTH_COOKIE_NAME
|
private readonly cookieName = DEFAULT_AUTH_COOKIE_NAME
|
||||||
|
private readonly authEnabled: boolean
|
||||||
|
|
||||||
constructor(private readonly init: AuthManagerInit, private readonly logger: Logger) {
|
constructor(private readonly init: AuthManagerInit, private readonly logger: Logger) {
|
||||||
|
this.authEnabled = !Boolean(init.dangerouslySkipAuth)
|
||||||
|
|
||||||
|
if (!this.authEnabled) {
|
||||||
|
this.authStore = null
|
||||||
|
this.tokenManager = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const authFilePath = resolveAuthFilePath(init.configPath)
|
const authFilePath = resolveAuthFilePath(init.configPath)
|
||||||
this.authStore = new AuthStore(authFilePath, logger.child({ component: "auth" }))
|
this.authStore = new AuthStore(authFilePath, logger.child({ component: "auth" }))
|
||||||
|
|
||||||
@@ -37,6 +47,10 @@ export class AuthManager {
|
|||||||
this.tokenManager = init.generateToken ? new TokenManager(60_000) : null
|
this.tokenManager = init.generateToken ? new TokenManager(60_000) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAuthEnabled(): boolean {
|
||||||
|
return this.authEnabled
|
||||||
|
}
|
||||||
|
|
||||||
getCookieName(): string {
|
getCookieName(): string {
|
||||||
return this.cookieName
|
return this.cookieName
|
||||||
}
|
}
|
||||||
@@ -56,19 +70,31 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateLogin(username: string, password: string): boolean {
|
validateLogin(username: string, password: string): boolean {
|
||||||
return this.authStore.validateCredentials(username, password)
|
if (!this.authEnabled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return this.requireAuthStore().validateCredentials(username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
createSession(username: string) {
|
createSession(username: string) {
|
||||||
|
if (!this.authEnabled) {
|
||||||
|
return { id: "auth-disabled", createdAt: Date.now(), username: this.init.username }
|
||||||
|
}
|
||||||
return this.sessionManager.createSession(username)
|
return this.sessionManager.createSession(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return this.authStore.getStatus()
|
if (!this.authEnabled) {
|
||||||
|
return { username: this.init.username, passwordUserProvided: false }
|
||||||
|
}
|
||||||
|
return this.requireAuthStore().getStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
setPassword(password: string) {
|
setPassword(password: string) {
|
||||||
return this.authStore.setPassword({ password, markUserProvided: true })
|
if (!this.authEnabled) {
|
||||||
|
throw new Error("Internal authentication is disabled")
|
||||||
|
}
|
||||||
|
return this.requireAuthStore().setPassword({ password, markUserProvided: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoopbackRequest(request: FastifyRequest): boolean {
|
isLoopbackRequest(request: FastifyRequest): boolean {
|
||||||
@@ -76,6 +102,12 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSessionFromRequest(request: FastifyRequest): { username: string; sessionId: string } | null {
|
getSessionFromRequest(request: FastifyRequest): { username: string; sessionId: string } | null {
|
||||||
|
if (!this.authEnabled) {
|
||||||
|
// When auth is disabled, treat all requests as authenticated.
|
||||||
|
// We still return a stable username so callers can display it.
|
||||||
|
return { username: this.init.username, sessionId: "auth-disabled" }
|
||||||
|
}
|
||||||
|
|
||||||
const cookies = parseCookies(request.headers.cookie)
|
const cookies = parseCookies(request.headers.cookie)
|
||||||
const sessionId = cookies[this.cookieName]
|
const sessionId = cookies[this.cookieName]
|
||||||
const session = this.sessionManager.getSession(sessionId)
|
const session = this.sessionManager.getSession(sessionId)
|
||||||
@@ -90,6 +122,13 @@ export class AuthManager {
|
|||||||
clearSessionCookie(reply: FastifyReply) {
|
clearSessionCookie(reply: FastifyReply) {
|
||||||
reply.header("Set-Cookie", buildSessionCookie(this.cookieName, "", { maxAgeSeconds: 0 }))
|
reply.header("Set-Cookie", buildSessionCookie(this.cookieName, "", { maxAgeSeconds: 0 }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private requireAuthStore(): AuthStore {
|
||||||
|
if (!this.authStore) {
|
||||||
|
throw new Error("Auth store is unavailable")
|
||||||
|
}
|
||||||
|
return this.authStore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAuthFilePath(configPath: string) {
|
function resolveAuthFilePath(configPath: string) {
|
||||||
|
|||||||
@@ -222,20 +222,18 @@ export class FileSystemBrowser {
|
|||||||
const results: FileSystemEntry[] = []
|
const results: FileSystemEntry[] = []
|
||||||
|
|
||||||
for (const entry of dirents) {
|
for (const entry of dirents) {
|
||||||
if (!options.includeFiles && !entry.isDirectory()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const absoluteEntryPath = path.join(directory, entry.name)
|
const absoluteEntryPath = path.join(directory, entry.name)
|
||||||
let stats: fs.Stats
|
let stats: fs.Stats
|
||||||
try {
|
try {
|
||||||
|
// Use fs.statSync (not Dirent.isDirectory) so symlinks to directories
|
||||||
|
// are treated as directories in directory-only listings.
|
||||||
stats = fs.statSync(absoluteEntryPath)
|
stats = fs.statSync(absoluteEntryPath)
|
||||||
} catch {
|
} catch {
|
||||||
// Skip entries we cannot stat (insufficient permissions, etc.)
|
// Skip entries we cannot stat (insufficient permissions, etc.)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirectory = entry.isDirectory()
|
const isDirectory = stats.isDirectory()
|
||||||
if (!options.includeFiles && !isDirectory) {
|
if (!options.includeFiles && !isDirectory) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ interface CliOptions {
|
|||||||
authUsername: string
|
authUsername: string
|
||||||
authPassword?: string
|
authPassword?: string
|
||||||
generateToken: boolean
|
generateToken: boolean
|
||||||
|
dangerouslySkipAuth: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PORT = 9898
|
const DEFAULT_PORT = 9898
|
||||||
@@ -84,6 +85,14 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
.env("CODENOMAD_GENERATE_TOKEN")
|
.env("CODENOMAD_GENERATE_TOKEN")
|
||||||
.default(false),
|
.default(false),
|
||||||
)
|
)
|
||||||
|
.addOption(
|
||||||
|
new Option(
|
||||||
|
"--dangerously-skip-auth",
|
||||||
|
"Disable CodeNomad's internal auth. Use only behind a trusted perimeter (SSO/VPN/etc).",
|
||||||
|
)
|
||||||
|
.env("CODENOMAD_SKIP_AUTH")
|
||||||
|
.default(false),
|
||||||
|
)
|
||||||
|
|
||||||
program.parse(argv, { from: "user" })
|
program.parse(argv, { from: "user" })
|
||||||
const parsed = program.opts<{
|
const parsed = program.opts<{
|
||||||
@@ -104,8 +113,14 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
username: string
|
username: string
|
||||||
password?: string
|
password?: string
|
||||||
generateToken?: boolean
|
generateToken?: boolean
|
||||||
|
dangerouslySkipAuth?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const parseBooleanEnv = (value: string | undefined): boolean => {
|
||||||
|
const normalized = (value ?? "").trim().toLowerCase()
|
||||||
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "y" || normalized === "on"
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedRoot = parsed.workspaceRoot ?? parsed.root ?? process.cwd()
|
const resolvedRoot = parsed.workspaceRoot ?? parsed.root ?? process.cwd()
|
||||||
|
|
||||||
const normalizedHost = resolveHost(parsed.host)
|
const normalizedHost = resolveHost(parsed.host)
|
||||||
@@ -130,6 +145,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
authUsername: parsed.username,
|
authUsername: parsed.username,
|
||||||
authPassword: parsed.password,
|
authPassword: parsed.password,
|
||||||
generateToken: Boolean(parsed.generateToken),
|
generateToken: Boolean(parsed.generateToken),
|
||||||
|
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +190,12 @@ async function main() {
|
|||||||
|
|
||||||
logger.info({ options: logOptions }, "Starting CodeNomad CLI server")
|
logger.info({ options: logOptions }, "Starting CodeNomad CLI server")
|
||||||
|
|
||||||
|
if (options.dangerouslySkipAuth) {
|
||||||
|
logger.warn(
|
||||||
|
"DANGEROUS: internal authentication is disabled (--dangerously-skip-auth / CODENOMAD_SKIP_AUTH).",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const eventBus = new EventBus(eventLogger)
|
const eventBus = new EventBus(eventLogger)
|
||||||
|
|
||||||
const isLoopbackHost = (host: string) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
|
const isLoopbackHost = (host: string) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
|
||||||
@@ -195,11 +217,12 @@ async function main() {
|
|||||||
username: options.authUsername,
|
username: options.authUsername,
|
||||||
password: options.authPassword,
|
password: options.authPassword,
|
||||||
generateToken: options.generateToken,
|
generateToken: options.generateToken,
|
||||||
|
dangerouslySkipAuth: options.dangerouslySkipAuth,
|
||||||
},
|
},
|
||||||
logger.child({ component: "auth" }),
|
logger.child({ component: "auth" }),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (options.generateToken) {
|
if (options.generateToken && !options.dangerouslySkipAuth) {
|
||||||
const token = authManager.issueBootstrapToken()
|
const token = authManager.issueBootstrapToken()
|
||||||
if (token) {
|
if (token) {
|
||||||
console.log(`${BOOTSTRAP_TOKEN_STDOUT_PREFIX}${token}`)
|
console.log(`${BOOTSTRAP_TOKEN_STDOUT_PREFIX}${token}`)
|
||||||
|
|||||||
@@ -380,6 +380,16 @@ async function proxyWorkspaceRequest(args: {
|
|||||||
if (instanceAuthHeader) {
|
if (instanceAuthHeader) {
|
||||||
headers.authorization = instanceAuthHeader
|
headers.authorization = instanceAuthHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enforce per-workspace directory scoping for all proxied OpenCode requests.
|
||||||
|
// OpenCode expects the *full* path; we send it via header to avoid query tampering.
|
||||||
|
const directory = workspace.path
|
||||||
|
const isNonASCII = /[^\x00-\x7F]/.test(directory)
|
||||||
|
const encodedDirectory = isNonASCII ? encodeURIComponent(directory) : directory
|
||||||
|
|
||||||
|
// Overwrite any client-provided value (case-insensitive headers are normalized by Node).
|
||||||
|
;(headers as Record<string, unknown>)["x-opencode-directory"] = encodedDirectory
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
},
|
},
|
||||||
onError: (proxyReply, { error }) => {
|
onError: (proxyReply, { error }) => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"dev:ui": "npm run dev --workspace @codenomad/ui",
|
"dev:ui": "npm run dev --workspace @codenomad/ui",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
name = "codenomad-tauri"
|
name = "codenomad-tauri"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.5.2", features = [] }
|
tauri-build = { version = "2.5.2", features = [] }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"identifier": "main-window-native-dialogs",
|
"identifier": "main-window-native-dialogs",
|
||||||
"description": "Grant the main window access to required core features and native dialog commands.",
|
"description": "Grant the main window access to required core features and native dialog commands.",
|
||||||
"remote": {
|
"remote": {
|
||||||
"urls": ["http://127.0.0.1:*", "http://localhost:*"]
|
"urls": ["http://127.0.0.1:*", "http://localhost:*", "http://tauri.localhost/*", "https://tauri.localhost/*"]
|
||||||
},
|
},
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant the main window access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:*"]},"local":true,"windows":["main"],"permissions":["core:default","core:menu:default","dialog:allow-open","opener:allow-default-urls","core:webview:allow-set-webview-zoom"]}}
|
{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant the main window access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:*","http://tauri.localhost/*","https://tauri.localhost/*"]},"local":true,"windows":["main"],"permissions":["core:default","core:menu:default","dialog:allow-open","opener:allow-default-urls","core:webview:allow-set-webview-zoom"]}}
|
||||||
@@ -464,13 +464,33 @@ impl CliProcessManager {
|
|||||||
let status_clone = status.clone();
|
let status_clone = status.clone();
|
||||||
let app_clone = app.clone();
|
let app_clone = app.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let code = {
|
// Do not hold the child mutex while waiting for process exit.
|
||||||
|
// Holding the lock across `wait()` deadlocks `stop()`, which needs the
|
||||||
|
// same lock to send SIGTERM/SIGKILL when the user quits the app.
|
||||||
|
let code = loop {
|
||||||
|
let maybe_exited = {
|
||||||
let mut guard = child_holder.lock();
|
let mut guard = child_holder.lock();
|
||||||
if let Some(child) = guard.as_mut() {
|
if guard.is_none() {
|
||||||
child.wait().ok()
|
return;
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
match guard
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|child| child.try_wait().ok().flatten())
|
||||||
|
{
|
||||||
|
Some(status) => {
|
||||||
|
// Drop the handle after the process exits so other callers
|
||||||
|
// don't attempt to stop/kill a finished process.
|
||||||
|
*guard = None;
|
||||||
|
Some(status)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(status) = maybe_exited {
|
||||||
|
break Some(status);
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut locked = status_clone.lock();
|
let mut locked = status_clone.lock();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod cli_manager;
|
|||||||
|
|
||||||
use cli_manager::{CliProcessManager, CliStatus};
|
use cli_manager::{CliProcessManager, CliStatus};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder};
|
use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder};
|
||||||
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
|
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
|
||||||
use tauri::webview::Webview;
|
use tauri::webview::Webview;
|
||||||
@@ -11,6 +12,8 @@ use tauri::{AppHandle, Emitter, Manager, Runtime, Wry};
|
|||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub manager: CliProcessManager,
|
pub manager: CliProcessManager,
|
||||||
@@ -39,7 +42,10 @@ fn is_dev_mode() -> bool {
|
|||||||
fn should_allow_internal(url: &Url) -> bool {
|
fn should_allow_internal(url: &Url) -> bool {
|
||||||
match url.scheme() {
|
match url.scheme() {
|
||||||
"tauri" | "asset" | "file" => true,
|
"tauri" | "asset" | "file" => true,
|
||||||
"http" | "https" => matches!(url.host_str(), Some("127.0.0.1" | "localhost")),
|
// On Windows/WebView2, Tauri serves the app assets from `tauri.localhost`.
|
||||||
|
// This must be treated as an internal origin or the navigation guard will
|
||||||
|
// redirect it to the system browser and the app will appear blank.
|
||||||
|
"http" | "https" => matches!(url.host_str(), Some("127.0.0.1" | "localhost" | "tauri.localhost")),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,6 +170,11 @@ fn main() {
|
|||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|app_handle, event| match event {
|
.run(|app_handle, event| match event {
|
||||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||||
|
// `app_handle.exit(0)` triggers another `ExitRequested`. Without a guard, we can
|
||||||
|
// prevent exit forever and the app never quits (Cmd+Q / Quit menu appears stuck).
|
||||||
|
if QUIT_REQUESTED.swap(true, Ordering::SeqCst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
api.prevent_exit();
|
api.prevent_exit();
|
||||||
let app = app_handle.clone();
|
let app = app_handle.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
@@ -178,6 +189,9 @@ fn main() {
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
// Ensure we have time to stop the CLI process before the app exits.
|
// Ensure we have time to stop the CLI process before the app exits.
|
||||||
|
if QUIT_REQUESTED.swap(true, Ordering::SeqCst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
let app = app_handle.clone();
|
let app = app_handle.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.9.2",
|
"version": "0.9.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
"@suid/icons-material": "^0.9.0",
|
"@suid/icons-material": "^0.9.0",
|
||||||
"@suid/material": "^0.19.0",
|
"@suid/material": "^0.19.0",
|
||||||
"@suid/system": "^0.14.0",
|
"@suid/system": "^0.14.0",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||||
"ansi-sequence-parser": "^1.1.3",
|
"ansi-sequence-parser": "^1.1.3",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { ChevronDown } from "lucide-solid"
|
|||||||
import type { Agent } from "../types/session"
|
import type { Agent } from "../types/session"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import Kbd from "./kbd"
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
|
|
||||||
@@ -113,9 +112,6 @@ export default function AgentSelector(props: AgentSelectorProps) {
|
|||||||
)}
|
)}
|
||||||
</Select.Value>
|
</Select.Value>
|
||||||
</div>
|
</div>
|
||||||
<span class="selector-trigger-hint selector-trigger-hint--top" aria-hidden="true">
|
|
||||||
<Kbd shortcut="cmd+shift+a" />
|
|
||||||
</span>
|
|
||||||
<Select.Icon class="selector-trigger-icon">
|
<Select.Icon class="selector-trigger-icon">
|
||||||
<ChevronDown class="w-3 h-3" />
|
<ChevronDown class="w-3 h-3" />
|
||||||
</Select.Icon>
|
</Select.Icon>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function CodeBlockInline(props: CodeBlockInlineProps) {
|
|||||||
|
|
||||||
const highlighted = highlighter.codeToHtml(props.code, {
|
const highlighted = highlighter.codeToHtml(props.code, {
|
||||||
lang: props.language as CodeToHtmlOptions["lang"],
|
lang: props.language as CodeToHtmlOptions["lang"],
|
||||||
theme: isDark() ? "github-dark" : "github-light",
|
theme: isDark() ? "github-dark" : "github-light-high-contrast",
|
||||||
})
|
})
|
||||||
setHtml(highlighted)
|
setHtml(highlighted)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useConfig } from "../stores/preferences"
|
|||||||
import AdvancedSettingsModal from "./advanced-settings-modal"
|
import AdvancedSettingsModal from "./advanced-settings-modal"
|
||||||
import DirectoryBrowserDialog from "./directory-browser-dialog"
|
import DirectoryBrowserDialog from "./directory-browser-dialog"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
|
import { ThemeModeToggle } from "./theme-mode-toggle"
|
||||||
import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions"
|
import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions"
|
||||||
import VersionPill from "./version-pill"
|
import VersionPill from "./version-pill"
|
||||||
import { DiscordSymbolIcon, GitHubMarkIcon } from "./brand-icons"
|
import { DiscordSymbolIcon, GitHubMarkIcon } from "./brand-icons"
|
||||||
@@ -313,8 +314,9 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
</Select.Portal>
|
</Select.Portal>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="absolute top-4 right-6 flex items-center gap-2">
|
||||||
|
<ThemeModeToggle class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center" />
|
||||||
<Show when={props.onOpenRemoteAccess}>
|
<Show when={props.onOpenRemoteAccess}>
|
||||||
<div class="absolute top-4 right-6">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
||||||
@@ -322,8 +324,8 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<MonitorUp class="w-4 h-4" />
|
<MonitorUp class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
<div class="mb-6 text-center shrink-0">
|
<div class="mb-6 text-center shrink-0">
|
||||||
<div class="mb-3 flex justify-center">
|
<div class="mb-3 flex justify-center">
|
||||||
<img src={codeNomadLogo} alt={t("folderSelection.logoAlt")} class="h-32 w-auto sm:h-48" loading="lazy" />
|
<img src={codeNomadLogo} alt={t("folderSelection.logoAlt")} class="h-32 w-auto sm:h-48" loading="lazy" />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import KeyboardHint from "./keyboard-hint"
|
|||||||
import { Plus, MonitorUp } from "lucide-solid"
|
import { Plus, MonitorUp } from "lucide-solid"
|
||||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import { ThemeModeToggle } from "./theme-mode-toggle"
|
||||||
|
|
||||||
interface InstanceTabsProps {
|
interface InstanceTabsProps {
|
||||||
instances: Map<string, Instance>
|
instances: Map<string, Instance>
|
||||||
@@ -52,6 +53,7 @@ const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<ThemeModeToggle class="new-tab-button" />
|
||||||
<Show when={Boolean(props.onOpenRemoteAccess)}>
|
<Show when={Boolean(props.onOpenRemoteAccess)}>
|
||||||
<button
|
<button
|
||||||
class="new-tab-button tab-remote-button"
|
class="new-tab-button tab-remote-button"
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { Accordion } from "@kobalte/core"
|
|||||||
import { ChevronDown, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
import { ChevronDown, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
||||||
import AppBar from "@suid/material/AppBar"
|
import AppBar from "@suid/material/AppBar"
|
||||||
import Box from "@suid/material/Box"
|
import Box from "@suid/material/Box"
|
||||||
import Divider from "@suid/material/Divider"
|
|
||||||
import Drawer from "@suid/material/Drawer"
|
import Drawer from "@suid/material/Drawer"
|
||||||
import IconButton from "@suid/material/IconButton"
|
import IconButton from "@suid/material/IconButton"
|
||||||
import Toolbar from "@suid/material/Toolbar"
|
import Toolbar from "@suid/material/Toolbar"
|
||||||
@@ -88,9 +87,9 @@ interface InstanceShellProps {
|
|||||||
tabBarOffset: number
|
tabBarOffset: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SESSION_SIDEBAR_WIDTH = 280
|
const DEFAULT_SESSION_SIDEBAR_WIDTH = 340
|
||||||
const MIN_SESSION_SIDEBAR_WIDTH = 220
|
const MIN_SESSION_SIDEBAR_WIDTH = 220
|
||||||
const MAX_SESSION_SIDEBAR_WIDTH = 360
|
const MAX_SESSION_SIDEBAR_WIDTH = 400
|
||||||
const RIGHT_DRAWER_WIDTH = 260
|
const RIGHT_DRAWER_WIDTH = 260
|
||||||
const MIN_RIGHT_DRAWER_WIDTH = 200
|
const MIN_RIGHT_DRAWER_WIDTH = 200
|
||||||
const MAX_RIGHT_DRAWER_WIDTH = 380
|
const MAX_RIGHT_DRAWER_WIDTH = 380
|
||||||
@@ -875,7 +874,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 text-primary">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -911,11 +910,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
void result.catch((error) => log.error("Failed to create session:", error))
|
void result.catch((error) => log.error("Failed to create session:", error))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
enableFilterBar
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
showFooter={false}
|
showFooter={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Divider />
|
<div class="session-sidebar-separator" />
|
||||||
<Show when={activeSessionForInstance()}>
|
<Show when={activeSessionForInstance()}>
|
||||||
{(activeSession) => (
|
{(activeSession) => (
|
||||||
<>
|
<>
|
||||||
@@ -936,6 +936,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ThinkingSelector instanceId={props.instance.id} currentModel={activeSession().model} />
|
<ThinkingSelector instanceId={props.instance.id} currentModel={activeSession().model} />
|
||||||
|
|
||||||
|
<div class="session-sidebar-selector-hints" aria-hidden="true">
|
||||||
|
<Kbd shortcut="cmd+shift+a" />
|
||||||
|
<Kbd shortcut="cmd+shift+m" />
|
||||||
|
<Kbd shortcut="cmd+shift+t" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1081,8 +1087,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full" ref={setRightDrawerContentEl}>
|
<div class="flex flex-col h-full" ref={setRightDrawerContentEl}>
|
||||||
<div class="flex items-center justify-between px-4 py-2 border-b border-base">
|
<div class="flex items-center justify-between px-4 py-2 border-b border-base text-primary">
|
||||||
<Typography variant="subtitle2" class="uppercase tracking-wide text-xs font-semibold">
|
<Typography variant="subtitle2" class="uppercase tracking-wide text-xs font-semibold text-primary">
|
||||||
{t("instanceShell.rightPanel.title")}
|
{t("instanceShell.rightPanel.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -1324,13 +1330,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
||||||
<span class="uppercase text-[10px] tracking-wide text-primary/70">
|
<span class="uppercase text-[10px] tracking-wide text-muted">
|
||||||
{t("instanceShell.metrics.usedLabel")}
|
{t("instanceShell.metrics.usedLabel")}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
||||||
<span class="uppercase text-[10px] tracking-wide text-primary/70">
|
<span class="uppercase text-[10px] tracking-wide text-muted">
|
||||||
{t("instanceShell.metrics.availableLabel")}
|
{t("instanceShell.metrics.availableLabel")}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
||||||
@@ -1354,13 +1360,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
|
|
||||||
<Show when={!showingInfoView()}>
|
<Show when={!showingInfoView()}>
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
||||||
<span class="uppercase text-[10px] tracking-wide text-primary/70">
|
<span class="uppercase text-[10px] tracking-wide text-muted">
|
||||||
{t("instanceShell.metrics.usedLabel")}
|
{t("instanceShell.metrics.usedLabel")}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
||||||
<span class="uppercase text-[10px] tracking-wide text-primary/70">
|
<span class="uppercase text-[10px] tracking-wide text-muted">
|
||||||
{t("instanceShell.metrics.availableLabel")}
|
{t("instanceShell.metrics.availableLabel")}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||||
import { renderMarkdown, onLanguagesLoaded, decodeHtmlEntities } from "../lib/markdown"
|
import { renderMarkdown, onLanguagesLoaded, decodeHtmlEntities, setMarkdownTheme } from "../lib/markdown"
|
||||||
import { useGlobalCache } from "../lib/hooks/use-global-cache"
|
import { useGlobalCache } from "../lib/hooks/use-global-cache"
|
||||||
import type { TextPart, RenderCache } from "../types/message"
|
import type { TextPart, RenderCache } from "../types/message"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
@@ -72,6 +72,9 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
const { part, text, themeKey, highlightEnabled, version } = resolved()
|
const { part, text, themeKey, highlightEnabled, version } = resolved()
|
||||||
|
|
||||||
|
// Ensure the markdown highlighter theme matches the active UI theme.
|
||||||
|
setMarkdownTheme(themeKey === "dark")
|
||||||
|
|
||||||
latestRequestedText = text
|
latestRequestedText = text
|
||||||
|
|
||||||
const cacheMatches = (cache: RenderCache | undefined) => {
|
const cacheMatches = (cache: RenderCache | undefined) => {
|
||||||
@@ -171,6 +174,8 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
|
|
||||||
const { part, text, themeKey, version } = resolved()
|
const { part, text, themeKey, version } = resolved()
|
||||||
|
|
||||||
|
setMarkdownTheme(themeKey === "dark")
|
||||||
|
|
||||||
if (latestRequestedText !== text) {
|
if (latestRequestedText !== text) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { messageStoreBus } from "../stores/message-v2/bus"
|
|||||||
import { formatTokenTotal } from "../lib/formatters"
|
import { formatTokenTotal } from "../lib/formatters"
|
||||||
import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions"
|
import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions"
|
||||||
import { setActiveInstanceId } from "../stores/instances"
|
import { setActiveInstanceId } from "../stores/instances"
|
||||||
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
|
import { deleteMessagePart } from "../stores/session-actions"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
|
||||||
const TOOL_ICON = "🔧"
|
const TOOL_ICON = "🔧"
|
||||||
@@ -172,21 +174,254 @@ messageStoreBus.onInstanceDestroyed(clearInstanceCaches)
|
|||||||
interface ContentDisplayItem {
|
interface ContentDisplayItem {
|
||||||
type: "content"
|
type: "content"
|
||||||
key: string
|
key: string
|
||||||
record: MessageRecord
|
messageId: string
|
||||||
parts: ClientPart[]
|
startPartId: string
|
||||||
messageInfo?: MessageInfo
|
|
||||||
isQueued: boolean
|
|
||||||
showAgentMeta?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolDisplayItem {
|
interface ToolDisplayItem {
|
||||||
type: "tool"
|
type: "tool"
|
||||||
key: string
|
key: string
|
||||||
toolPart: ToolCallPart
|
|
||||||
messageInfo?: MessageInfo
|
|
||||||
messageId: string
|
messageId: string
|
||||||
messageVersion: number
|
partId: string
|
||||||
partVersion: number
|
}
|
||||||
|
|
||||||
|
interface MessageContentItemProps {
|
||||||
|
instanceId: string
|
||||||
|
sessionId: string
|
||||||
|
store: () => InstanceMessageStore
|
||||||
|
messageId: string
|
||||||
|
startPartId: string
|
||||||
|
messageIndex: number
|
||||||
|
lastAssistantIndex: () => number
|
||||||
|
onRevert?: (messageId: string) => void
|
||||||
|
onFork?: (messageId?: string) => void
|
||||||
|
onContentRendered?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function MessageContentItem(props: MessageContentItemProps) {
|
||||||
|
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||||
|
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||||
|
|
||||||
|
const isQueued = createMemo(() => {
|
||||||
|
const current = record()
|
||||||
|
if (!current) return false
|
||||||
|
if (current.role !== "user") return false
|
||||||
|
const lastAssistant = props.lastAssistantIndex()
|
||||||
|
return lastAssistant === -1 || props.messageIndex > lastAssistant
|
||||||
|
})
|
||||||
|
|
||||||
|
const parts = createMemo<ClientPart[]>(() => {
|
||||||
|
const current = record()
|
||||||
|
if (!current) return []
|
||||||
|
const ids = current.partIds
|
||||||
|
const startIndex = ids.indexOf(props.startPartId)
|
||||||
|
if (startIndex === -1) return []
|
||||||
|
|
||||||
|
const resolved: ClientPart[] = []
|
||||||
|
for (let idx = startIndex; idx < ids.length; idx++) {
|
||||||
|
const partId = ids[idx]
|
||||||
|
const part = current.parts[partId]?.data
|
||||||
|
if (!part) continue
|
||||||
|
if (
|
||||||
|
part.type === "tool" ||
|
||||||
|
part.type === "reasoning" ||
|
||||||
|
part.type === "compaction" ||
|
||||||
|
part.type === "step-start" ||
|
||||||
|
part.type === "step-finish"
|
||||||
|
) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
resolved.push(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
})
|
||||||
|
|
||||||
|
const showAgentMeta = createMemo(() => {
|
||||||
|
const current = record()
|
||||||
|
if (!current) return false
|
||||||
|
if (current.role !== "assistant") return false
|
||||||
|
|
||||||
|
const currentParts = parts()
|
||||||
|
if (!currentParts.some((part) => partHasRenderableText(part))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = current.partIds
|
||||||
|
const startIndex = ids.indexOf(props.startPartId)
|
||||||
|
if (startIndex === -1) return false
|
||||||
|
|
||||||
|
// Only show agent meta on the first content segment that contains renderable content.
|
||||||
|
for (let idx = 0; idx < startIndex; idx++) {
|
||||||
|
const partId = ids[idx]
|
||||||
|
const part = current.parts[partId]?.data
|
||||||
|
if (!part) continue
|
||||||
|
if (
|
||||||
|
part.type === "tool" ||
|
||||||
|
part.type === "reasoning" ||
|
||||||
|
part.type === "compaction" ||
|
||||||
|
part.type === "step-start" ||
|
||||||
|
part.type === "step-finish"
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (partHasRenderableText(part)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={record()}>
|
||||||
|
{(resolvedRecord) => (
|
||||||
|
<MessageItem
|
||||||
|
record={resolvedRecord()}
|
||||||
|
messageInfo={messageInfo()}
|
||||||
|
parts={parts()}
|
||||||
|
instanceId={props.instanceId}
|
||||||
|
sessionId={props.sessionId}
|
||||||
|
isQueued={isQueued()}
|
||||||
|
showAgentMeta={showAgentMeta()}
|
||||||
|
onRevert={props.onRevert}
|
||||||
|
onFork={props.onFork}
|
||||||
|
onContentRendered={props.onContentRendered}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolCallItemProps {
|
||||||
|
instanceId: string
|
||||||
|
sessionId: string
|
||||||
|
store: () => InstanceMessageStore
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
|
onContentRendered?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolCallItem(props: ToolCallItemProps) {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const [deleting, setDeleting] = createSignal(false)
|
||||||
|
|
||||||
|
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||||
|
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||||
|
const partEntry = createMemo(() => record()?.parts?.[props.partId])
|
||||||
|
|
||||||
|
const toolPart = createMemo(() => {
|
||||||
|
const part = partEntry()?.data as ClientPart | undefined
|
||||||
|
if (!part || part.type !== "tool") return undefined
|
||||||
|
return part as ToolCallPart
|
||||||
|
})
|
||||||
|
|
||||||
|
const toolState = createMemo(() => toolPart()?.state as ToolState | undefined)
|
||||||
|
const toolName = createMemo(() => toolPart()?.tool || "")
|
||||||
|
const messageVersion = createMemo(() => record()?.revision ?? 0)
|
||||||
|
const partVersion = createMemo(() => partEntry()?.revision ?? 0)
|
||||||
|
|
||||||
|
const deleteDisabled = createMemo(() => {
|
||||||
|
if (deleting()) return true
|
||||||
|
// Avoid deleting while a tool is actively running to prevent confusing UI states.
|
||||||
|
if (isToolStateRunning(toolState())) return true
|
||||||
|
// Avoid deleting permission prompts from here; those are interactive.
|
||||||
|
return Boolean(toolPart()?.pendingPermission)
|
||||||
|
})
|
||||||
|
|
||||||
|
const taskSessionId = createMemo(() => {
|
||||||
|
const state = toolState()
|
||||||
|
if (!state) return ""
|
||||||
|
if (!(isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return extractTaskSessionId(state)
|
||||||
|
})
|
||||||
|
|
||||||
|
const taskLocation = createMemo(() => {
|
||||||
|
const id = taskSessionId()
|
||||||
|
if (!id) return null
|
||||||
|
return findTaskSessionLocation(id, props.instanceId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleGoToTaskSession = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
const location = taskLocation()
|
||||||
|
if (!location) return
|
||||||
|
navigateToTaskSession(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteToolPart = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
if (deleteDisabled()) return
|
||||||
|
|
||||||
|
setDeleting(true)
|
||||||
|
try {
|
||||||
|
await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messageBlock.tool.deletePart.failed.message"), {
|
||||||
|
title: t("messageBlock.tool.deletePart.failed.title"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={toolPart()}>
|
||||||
|
{(resolvedToolPart) => (
|
||||||
|
<>
|
||||||
|
<div class="tool-call-header-label">
|
||||||
|
<div class="tool-call-header-meta">
|
||||||
|
<span class="tool-call-icon">{TOOL_ICON}</span>
|
||||||
|
<span>{t("messageBlock.tool.header")}</span>
|
||||||
|
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Show when={taskSessionId()}>
|
||||||
|
<button
|
||||||
|
class="tool-call-header-button"
|
||||||
|
type="button"
|
||||||
|
disabled={!taskLocation()}
|
||||||
|
onClick={handleGoToTaskSession}
|
||||||
|
title={!taskLocation() ? t("messageBlock.tool.goToSession.unavailableTitle") : t("messageBlock.tool.goToSession.title")}
|
||||||
|
>
|
||||||
|
{t("messageBlock.tool.goToSession.label")}
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="tool-call-header-button"
|
||||||
|
type="button"
|
||||||
|
disabled={deleteDisabled()}
|
||||||
|
onClick={handleDeleteToolPart}
|
||||||
|
title={t("messageBlock.tool.deletePart.title")}
|
||||||
|
>
|
||||||
|
{deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ToolCall
|
||||||
|
toolCall={resolvedToolPart()}
|
||||||
|
toolCallId={props.partId}
|
||||||
|
messageId={props.messageId}
|
||||||
|
messageVersion={messageVersion()}
|
||||||
|
partVersion={partVersion()}
|
||||||
|
instanceId={props.instanceId}
|
||||||
|
sessionId={props.sessionId}
|
||||||
|
onContentRendered={props.onContentRendered}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StepDisplayItem {
|
interface StepDisplayItem {
|
||||||
@@ -204,6 +439,8 @@ type ReasoningDisplayItem = {
|
|||||||
messageInfo?: MessageInfo
|
messageInfo?: MessageInfo
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
defaultExpanded: boolean
|
defaultExpanded: boolean
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompactionDisplayItem = {
|
type CompactionDisplayItem = {
|
||||||
@@ -212,6 +449,8 @@ type CompactionDisplayItem = {
|
|||||||
part: ClientPart
|
part: ClientPart
|
||||||
messageInfo?: MessageInfo
|
messageInfo?: MessageInfo
|
||||||
accentColor?: string
|
accentColor?: string
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageBlockItem = ContentDisplayItem | ToolDisplayItem | StepDisplayItem | ReasoningDisplayItem | CompactionDisplayItem
|
type MessageBlockItem = ContentDisplayItem | ToolDisplayItem | StepDisplayItem | ReasoningDisplayItem | CompactionDisplayItem
|
||||||
@@ -272,7 +511,6 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
const items: MessageBlockItem[] = []
|
const items: MessageBlockItem[] = []
|
||||||
const blockContentKeys: string[] = []
|
const blockContentKeys: string[] = []
|
||||||
const blockToolKeys: string[] = []
|
const blockToolKeys: string[] = []
|
||||||
let segmentIndex = 0
|
|
||||||
let pendingParts: ClientPart[] = []
|
let pendingParts: ClientPart[] = []
|
||||||
let agentMetaAttached = current.role !== "assistant"
|
let agentMetaAttached = current.role !== "assistant"
|
||||||
const defaultAccentColor = current.role === "user" ? USER_BORDER_COLOR : ASSISTANT_BORDER_COLOR
|
const defaultAccentColor = current.role === "user" ? USER_BORDER_COLOR : ASSISTANT_BORDER_COLOR
|
||||||
@@ -280,34 +518,28 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
|
|
||||||
const flushContent = () => {
|
const flushContent = () => {
|
||||||
if (pendingParts.length === 0) return
|
if (pendingParts.length === 0) return
|
||||||
const segmentKey = `${current.id}:segment:${segmentIndex}`
|
const startPartId = typeof (pendingParts[0] as any)?.id === "string" ? ((pendingParts[0] as any).id as string) : ""
|
||||||
segmentIndex += 1
|
if (!startPartId) {
|
||||||
const shouldShowAgentMeta =
|
pendingParts = []
|
||||||
current.role === "assistant" &&
|
return
|
||||||
!agentMetaAttached &&
|
}
|
||||||
pendingParts.some((part) => partHasRenderableText(part))
|
|
||||||
|
if (!agentMetaAttached && pendingParts.some((part) => partHasRenderableText(part))) {
|
||||||
|
agentMetaAttached = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentKey = `${current.id}:content:${startPartId}`
|
||||||
let cached = sessionCache.messageItems.get(segmentKey)
|
let cached = sessionCache.messageItems.get(segmentKey)
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
cached = {
|
cached = {
|
||||||
type: "content",
|
type: "content",
|
||||||
key: segmentKey,
|
key: segmentKey,
|
||||||
record: current,
|
messageId: current.id,
|
||||||
parts: pendingParts.slice(),
|
startPartId,
|
||||||
messageInfo: info,
|
|
||||||
isQueued,
|
|
||||||
showAgentMeta: shouldShowAgentMeta,
|
|
||||||
}
|
}
|
||||||
sessionCache.messageItems.set(segmentKey, cached)
|
sessionCache.messageItems.set(segmentKey, cached)
|
||||||
} else {
|
|
||||||
cached.record = current
|
|
||||||
cached.parts = pendingParts.slice()
|
|
||||||
cached.messageInfo = info
|
|
||||||
cached.isQueued = isQueued
|
|
||||||
cached.showAgentMeta = shouldShowAgentMeta
|
|
||||||
}
|
|
||||||
if (shouldShowAgentMeta) {
|
|
||||||
agentMetaAttached = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(cached)
|
items.push(cached)
|
||||||
blockContentKeys.push(segmentKey)
|
blockContentKeys.push(segmentKey)
|
||||||
lastAccentColor = defaultAccentColor
|
lastAccentColor = defaultAccentColor
|
||||||
@@ -317,28 +549,26 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
orderedParts.forEach((part, partIndex) => {
|
orderedParts.forEach((part, partIndex) => {
|
||||||
if (part.type === "tool") {
|
if (part.type === "tool") {
|
||||||
flushContent()
|
flushContent()
|
||||||
const partVersion = typeof (part as any).revision === "number" ? (part as any).revision : 0
|
const partId = part.id
|
||||||
const messageVersion = current.revision
|
if (!partId) {
|
||||||
const key = `${current.id}:${part.id ?? partIndex}`
|
// Tool parts are required to have ids; if one slips through, skip rendering
|
||||||
|
// to avoid unstable keys and accidental remount cascades.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const key = `${current.id}:${partId}`
|
||||||
let toolItem = sessionCache.toolItems.get(key)
|
let toolItem = sessionCache.toolItems.get(key)
|
||||||
if (!toolItem) {
|
if (!toolItem) {
|
||||||
toolItem = {
|
toolItem = {
|
||||||
type: "tool",
|
type: "tool",
|
||||||
key,
|
key,
|
||||||
toolPart: part as ToolCallPart,
|
|
||||||
messageInfo: info,
|
|
||||||
messageId: current.id,
|
messageId: current.id,
|
||||||
messageVersion,
|
partId,
|
||||||
partVersion,
|
|
||||||
}
|
}
|
||||||
sessionCache.toolItems.set(key, toolItem)
|
sessionCache.toolItems.set(key, toolItem)
|
||||||
} else {
|
} else {
|
||||||
toolItem.key = key
|
toolItem.key = key
|
||||||
toolItem.toolPart = part as ToolCallPart
|
|
||||||
toolItem.messageInfo = info
|
|
||||||
toolItem.messageId = current.id
|
toolItem.messageId = current.id
|
||||||
toolItem.messageVersion = messageVersion
|
toolItem.partId = partId
|
||||||
toolItem.partVersion = partVersion
|
|
||||||
}
|
}
|
||||||
items.push(toolItem)
|
items.push(toolItem)
|
||||||
blockToolKeys.push(key)
|
blockToolKeys.push(key)
|
||||||
@@ -348,7 +578,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
|
|
||||||
if (part.type === "compaction") {
|
if (part.type === "compaction") {
|
||||||
flushContent()
|
flushContent()
|
||||||
const key = `${current.id}:${part.id ?? partIndex}:compaction`
|
const partId = part.id ?? ""
|
||||||
|
const key = `${current.id}:${partId || partIndex}:compaction`
|
||||||
const isAuto = Boolean((part as any)?.auto)
|
const isAuto = Boolean((part as any)?.auto)
|
||||||
items.push({
|
items.push({
|
||||||
type: "compaction",
|
type: "compaction",
|
||||||
@@ -356,6 +587,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
part,
|
part,
|
||||||
messageInfo: info,
|
messageInfo: info,
|
||||||
accentColor: isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR,
|
accentColor: isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR,
|
||||||
|
messageId: current.id,
|
||||||
|
partId,
|
||||||
})
|
})
|
||||||
lastAccentColor = isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR
|
lastAccentColor = isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR
|
||||||
return
|
return
|
||||||
@@ -380,7 +613,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
if (part.type === "reasoning") {
|
if (part.type === "reasoning") {
|
||||||
flushContent()
|
flushContent()
|
||||||
if (props.showThinking() && reasoningHasRenderableContent(part)) {
|
if (props.showThinking() && reasoningHasRenderableContent(part)) {
|
||||||
const key = `${current.id}:${part.id ?? partIndex}:reasoning`
|
const partId = part.id ?? ""
|
||||||
|
const key = `${current.id}:${partId || partIndex}:reasoning`
|
||||||
const showAgentMeta = current.role === "assistant" && !agentMetaAttached
|
const showAgentMeta = current.role === "assistant" && !agentMetaAttached
|
||||||
if (showAgentMeta) {
|
if (showAgentMeta) {
|
||||||
agentMetaAttached = true
|
agentMetaAttached = true
|
||||||
@@ -392,6 +626,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageInfo: info,
|
messageInfo: info,
|
||||||
showAgentMeta,
|
showAgentMeta,
|
||||||
defaultExpanded: props.thinkingDefaultExpanded(),
|
defaultExpanded: props.thinkingDefaultExpanded(),
|
||||||
|
messageId: current.id,
|
||||||
|
partId,
|
||||||
})
|
})
|
||||||
lastAccentColor = ASSISTANT_BORDER_COLOR
|
lastAccentColor = ASSISTANT_BORDER_COLOR
|
||||||
}
|
}
|
||||||
@@ -427,21 +663,21 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={block()} keyed>
|
<Show when={block()}>
|
||||||
{(resolvedBlock) => (
|
{(resolvedBlock) => (
|
||||||
<div class="message-stream-block" data-message-id={resolvedBlock.record.id}>
|
<div class="message-stream-block" data-message-id={resolvedBlock().record.id}>
|
||||||
<For each={resolvedBlock.items}>
|
<For each={resolvedBlock().items}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={item.type === "content"}>
|
<Match when={item.type === "content"}>
|
||||||
<MessageItem
|
<MessageContentItem
|
||||||
record={(item as ContentDisplayItem).record}
|
|
||||||
messageInfo={(item as ContentDisplayItem).messageInfo}
|
|
||||||
parts={(item as ContentDisplayItem).parts}
|
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
isQueued={(item as ContentDisplayItem).isQueued}
|
store={props.store}
|
||||||
showAgentMeta={(item as ContentDisplayItem).showAgentMeta}
|
messageId={(item as ContentDisplayItem).messageId}
|
||||||
|
startPartId={(item as ContentDisplayItem).startPartId}
|
||||||
|
messageIndex={props.messageIndex}
|
||||||
|
lastAssistantIndex={props.lastAssistantIndex}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
@@ -450,46 +686,14 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
<Match when={item.type === "tool"}>
|
<Match when={item.type === "tool"}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const toolItem = item as ToolDisplayItem
|
const toolItem = item as ToolDisplayItem
|
||||||
const toolState = toolItem.toolPart.state as ToolState | undefined
|
|
||||||
const hasToolState =
|
|
||||||
Boolean(toolState) && (isToolStateRunning(toolState) || isToolStateCompleted(toolState) || isToolStateError(toolState))
|
|
||||||
const taskSessionId = hasToolState ? extractTaskSessionId(toolState) : ""
|
|
||||||
const taskLocation = taskSessionId ? findTaskSessionLocation(taskSessionId, props.instanceId) : null
|
|
||||||
const handleGoToTaskSession = (event: MouseEvent) => {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
if (!taskLocation) return
|
|
||||||
navigateToTaskSession(taskLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="tool-call-message" data-key={toolItem.key}>
|
<div class="tool-call-message" data-key={toolItem.key}>
|
||||||
<div class="tool-call-header-label">
|
<ToolCallItem
|
||||||
<div class="tool-call-header-meta">
|
|
||||||
<span class="tool-call-icon">{TOOL_ICON}</span>
|
|
||||||
<span>{t("messageBlock.tool.header")}</span>
|
|
||||||
<span class="tool-name">{toolItem.toolPart.tool || t("messageBlock.tool.unknown")}</span>
|
|
||||||
</div>
|
|
||||||
<Show when={taskSessionId}>
|
|
||||||
<button
|
|
||||||
class="tool-call-header-button"
|
|
||||||
type="button"
|
|
||||||
disabled={!taskLocation}
|
|
||||||
onClick={handleGoToTaskSession}
|
|
||||||
title={!taskLocation ? t("messageBlock.tool.goToSession.unavailableTitle") : t("messageBlock.tool.goToSession.title")}
|
|
||||||
>
|
|
||||||
{t("messageBlock.tool.goToSession.label")}
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<ToolCall
|
|
||||||
toolCall={toolItem.toolPart}
|
|
||||||
toolCallId={toolItem.toolPart.id}
|
|
||||||
messageId={toolItem.messageId}
|
|
||||||
messageVersion={toolItem.messageVersion}
|
|
||||||
partVersion={toolItem.partVersion}
|
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
|
store={props.store}
|
||||||
|
messageId={toolItem.messageId}
|
||||||
|
partId={toolItem.partId}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -497,7 +701,12 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
})()}
|
})()}
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "step-start"}>
|
<Match when={item.type === "step-start"}>
|
||||||
<StepCard kind="start" part={(item as StepDisplayItem).part} messageInfo={(item as StepDisplayItem).messageInfo} showAgentMeta />
|
<StepCard
|
||||||
|
kind="start"
|
||||||
|
part={(item as StepDisplayItem).part}
|
||||||
|
messageInfo={(item as StepDisplayItem).messageInfo}
|
||||||
|
showAgentMeta
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "step-finish"}>
|
<Match when={item.type === "step-finish"}>
|
||||||
<StepCard
|
<StepCard
|
||||||
@@ -509,7 +718,15 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "compaction"}>
|
<Match when={item.type === "compaction"}>
|
||||||
<CompactionCard part={(item as CompactionDisplayItem).part} messageInfo={(item as CompactionDisplayItem).messageInfo} borderColor={(item as CompactionDisplayItem).accentColor} />
|
<CompactionCard
|
||||||
|
part={(item as CompactionDisplayItem).part}
|
||||||
|
messageInfo={(item as CompactionDisplayItem).messageInfo}
|
||||||
|
borderColor={(item as CompactionDisplayItem).accentColor}
|
||||||
|
instanceId={props.instanceId}
|
||||||
|
sessionId={props.sessionId}
|
||||||
|
messageId={(item as CompactionDisplayItem).messageId}
|
||||||
|
partId={(item as CompactionDisplayItem).partId}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "reasoning"}>
|
<Match when={item.type === "reasoning"}>
|
||||||
<ReasoningCard
|
<ReasoningCard
|
||||||
@@ -517,6 +734,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageInfo={(item as ReasoningDisplayItem).messageInfo}
|
messageInfo={(item as ReasoningDisplayItem).messageInfo}
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
|
messageId={(item as ReasoningDisplayItem).messageId}
|
||||||
|
partId={(item as ReasoningDisplayItem).partId}
|
||||||
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
|
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
|
||||||
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
|
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
|
||||||
/>
|
/>
|
||||||
@@ -539,8 +758,19 @@ interface StepCardProps {
|
|||||||
borderColor?: string
|
borderColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; borderColor?: string }) {
|
interface CompactionCardProps {
|
||||||
|
part: ClientPart
|
||||||
|
messageInfo?: MessageInfo
|
||||||
|
borderColor?: string
|
||||||
|
instanceId: string
|
||||||
|
sessionId: string
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompactionCard(props: CompactionCardProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const [deleting, setDeleting] = createSignal(false)
|
||||||
const isAuto = () => Boolean((props.part as any)?.auto)
|
const isAuto = () => Boolean((props.part as any)?.auto)
|
||||||
const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel"))
|
const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel"))
|
||||||
const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR)
|
const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR)
|
||||||
@@ -548,13 +778,43 @@ function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; bo
|
|||||||
const containerClass = () =>
|
const containerClass = () =>
|
||||||
`message-compaction-card ${isAuto() ? "message-compaction-card--auto" : "message-compaction-card--manual"}`
|
`message-compaction-card ${isAuto() ? "message-compaction-card--auto" : "message-compaction-card--manual"}`
|
||||||
|
|
||||||
|
const canDelete = () => Boolean(props.partId) && !deleting()
|
||||||
|
|
||||||
|
const handleDelete = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!canDelete()) return
|
||||||
|
setDeleting(true)
|
||||||
|
try {
|
||||||
|
await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messagePart.actions.deleteFailedMessage"), {
|
||||||
|
title: t("messagePart.actions.deleteFailedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={containerClass()}
|
class={`${containerClass()} relative`}
|
||||||
style={{ "border-left": `4px solid ${borderColor()}` }}
|
style={{ "border-left": `4px solid ${borderColor()}` }}
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={t("messageBlock.compaction.ariaLabel")}
|
aria-label={t("messageBlock.compaction.ariaLabel")}
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-header-button absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
|
disabled={!canDelete()}
|
||||||
|
onClick={handleDelete}
|
||||||
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
|
>
|
||||||
|
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="message-compaction-row">
|
<div class="message-compaction-row">
|
||||||
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
|
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
|
||||||
<span class="message-compaction-label">{label()}</span>
|
<span class="message-compaction-label">{label()}</span>
|
||||||
@@ -609,6 +869,7 @@ function StepCard(props: StepCardProps) {
|
|||||||
|
|
||||||
const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
|
const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
|
||||||
|
|
||||||
|
|
||||||
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
|
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
|
||||||
const entries = [
|
const entries = [
|
||||||
{ label: t("messageBlock.usage.input"), value: usage.input, formatter: formatTokenTotal },
|
{ label: t("messageBlock.usage.input"), value: usage.input, formatter: formatTokenTotal },
|
||||||
@@ -674,6 +935,8 @@ interface ReasoningCardProps {
|
|||||||
messageInfo?: MessageInfo
|
messageInfo?: MessageInfo
|
||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
defaultExpanded?: boolean
|
defaultExpanded?: boolean
|
||||||
}
|
}
|
||||||
@@ -681,6 +944,7 @@ interface ReasoningCardProps {
|
|||||||
function ReasoningCard(props: ReasoningCardProps) {
|
function ReasoningCard(props: ReasoningCardProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
|
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
|
||||||
|
const [deleting, setDeleting] = createSignal(false)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setExpanded(Boolean(props.defaultExpanded))
|
setExpanded(Boolean(props.defaultExpanded))
|
||||||
@@ -744,6 +1008,27 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
|
|
||||||
const toggle = () => setExpanded((prev) => !prev)
|
const toggle = () => setExpanded((prev) => !prev)
|
||||||
|
|
||||||
|
const hasDeleteTarget = () => Boolean(props.partId)
|
||||||
|
const canDelete = () => hasDeleteTarget() && !deleting()
|
||||||
|
|
||||||
|
const handleDelete = async (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!canDelete()) return
|
||||||
|
setDeleting(true)
|
||||||
|
try {
|
||||||
|
await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messagePart.actions.deleteFailedMessage"), {
|
||||||
|
title: t("messagePart.actions.deleteFailedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="message-reasoning-card">
|
<div class="message-reasoning-card">
|
||||||
<button
|
<button
|
||||||
@@ -774,6 +1059,25 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
<span class="message-reasoning-indicator">
|
<span class="message-reasoning-indicator">
|
||||||
{expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")}
|
{expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<Show when={hasDeleteTarget()}>
|
||||||
|
<span
|
||||||
|
class={`message-reasoning-indicator${canDelete() ? "" : " opacity-50 pointer-events-none"}`}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={handleDelete}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
handleDelete(event)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
|
>
|
||||||
|
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<span class="message-reasoning-time">{timestamp()}</span>
|
<span class="message-reasoning-time">{timestamp()}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type { MessageRecord } from "../stores/message-v2/types"
|
|||||||
import MessagePart from "./message-part"
|
import MessagePart from "./message-part"
|
||||||
import { copyToClipboard } from "../lib/clipboard"
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
|
import { deleteMessagePart } from "../stores/session-actions"
|
||||||
|
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
record: MessageRecord
|
record: MessageRecord
|
||||||
@@ -22,6 +24,7 @@ interface MessageItemProps {
|
|||||||
export default function MessageItem(props: MessageItemProps) {
|
export default function MessageItem(props: MessageItemProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [copied, setCopied] = createSignal(false)
|
const [copied, setCopied] = createSignal(false)
|
||||||
|
const [deletingParts, setDeletingParts] = createSignal<Set<string>>(new Set())
|
||||||
|
|
||||||
const isUser = () => props.record.role === "user"
|
const isUser = () => props.record.role === "user"
|
||||||
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
|
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
|
||||||
@@ -137,8 +140,17 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isGenerating = () => {
|
const isGenerating = () => {
|
||||||
|
if (hasContent()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the local record status for streaming placeholders.
|
||||||
|
if (!isUser() && props.record.status === "streaming") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const info = props.messageInfo
|
const info = props.messageInfo
|
||||||
return !hasContent() && info && info.role === "assistant" && info.time.completed !== undefined && info.time.completed === 0
|
return Boolean(info && info.role === "assistant" && info.time.completed !== undefined && info.time.completed === 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRevert = () => {
|
const handleRevert = () => {
|
||||||
@@ -163,7 +175,51 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isUser() && !hasContent()) {
|
const deletableTextPartId = () => {
|
||||||
|
const part = props.parts.find((candidate) => {
|
||||||
|
if (!candidate || candidate.type !== "text") return false
|
||||||
|
const id = (candidate as any).id
|
||||||
|
if (typeof id !== "string" || id.length === 0) return false
|
||||||
|
return !Boolean((candidate as any).synthetic)
|
||||||
|
})
|
||||||
|
return (part as any)?.id as string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDeletingPart = (partId?: string) => {
|
||||||
|
if (!partId) return false
|
||||||
|
return deletingParts().has(partId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPartDeleting = (partId: string, value: boolean) => {
|
||||||
|
setDeletingParts((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (value) {
|
||||||
|
next.add(partId)
|
||||||
|
} else {
|
||||||
|
next.delete(partId)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeletePart = async (partId?: string) => {
|
||||||
|
if (!partId) return
|
||||||
|
if (isDeletingPart(partId)) return
|
||||||
|
setPartDeleting(partId, true)
|
||||||
|
try {
|
||||||
|
await deleteMessagePart(props.instanceId, props.sessionId, props.record.id, partId)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messagePart.actions.deleteFailedMessage"), {
|
||||||
|
title: t("messagePart.actions.deleteFailedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setPartDeleting(partId, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUser() && !hasContent() && !isGenerating()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,9 +304,23 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
{t("messageItem.actions.copied")}
|
{t("messageItem.actions.copied")}
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
|
<Show when={deletableTextPartId()}>
|
||||||
|
{(partId) => (
|
||||||
|
<button
|
||||||
|
class="message-action-button"
|
||||||
|
onClick={() => void handleDeletePart(partId())}
|
||||||
|
disabled={isDeletingPart(partId())}
|
||||||
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
|
>
|
||||||
|
{isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!isUser()}>
|
<Show when={!isUser()}>
|
||||||
|
<div class="message-action-group">
|
||||||
<button
|
<button
|
||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
@@ -261,6 +331,21 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
{t("messageItem.actions.copied")}
|
{t("messageItem.actions.copied")}
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<Show when={deletableTextPartId()}>
|
||||||
|
{(partId) => (
|
||||||
|
<button
|
||||||
|
class="message-action-button"
|
||||||
|
onClick={() => void handleDeletePart(partId())}
|
||||||
|
disabled={isDeletingPart(partId())}
|
||||||
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
|
>
|
||||||
|
{isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
|
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,6 +413,19 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12l4 4 4-4m-4-8v12" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12l4 4 4-4m-4-8v12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void handleDeletePart(attachment.id)}
|
||||||
|
class="attachment-remove"
|
||||||
|
disabled={isDeletingPart(attachment.id)}
|
||||||
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
|
>
|
||||||
|
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<Show when={isImage}>
|
<Show when={isImage}>
|
||||||
<div class="attachment-chip-preview">
|
<div class="attachment-chip-preview">
|
||||||
<img src={attachment.url} alt={name} />
|
<img src={attachment.url} alt={name} />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Kbd from "./kbd"
|
|||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
|
||||||
const METRIC_CHIP_CLASS = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary"
|
const METRIC_CHIP_CLASS = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary"
|
||||||
const METRIC_LABEL_CLASS = "uppercase text-[10px] tracking-wide text-primary/70"
|
const METRIC_LABEL_CLASS = "uppercase text-[10px] tracking-wide text-muted"
|
||||||
|
|
||||||
interface MessageListHeaderProps {
|
interface MessageListHeaderProps {
|
||||||
usedTokens: number
|
usedTokens: number
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ interface MessagePartProps {
|
|||||||
const isAssistantMessage = () => props.messageType === "assistant"
|
const isAssistantMessage = () => props.messageType === "assistant"
|
||||||
const textContainerClass = () => (isAssistantMessage() ? "message-text message-text-assistant" : "message-text")
|
const textContainerClass = () => (isAssistantMessage() ? "message-text message-text-assistant" : "message-text")
|
||||||
|
|
||||||
|
const shouldHideTextPart = () => {
|
||||||
|
const part = props.part
|
||||||
|
if (!part || part.type !== "text") return false
|
||||||
|
// Keep optimistic user prompts visible; hide synthetic assistant text.
|
||||||
|
return Boolean((part as any).synthetic) && props.messageType !== "user"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const plainTextContent = () => {
|
const plainTextContent = () => {
|
||||||
const part = props.part
|
const part = props.part
|
||||||
|
|
||||||
@@ -94,11 +102,11 @@ interface MessagePartProps {
|
|||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={partType() === "text"}>
|
<Match when={partType() === "text"}>
|
||||||
<Show when={!(props.part.type === "text" && props.part.synthetic) && partHasRenderableText(props.part)}>
|
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
||||||
<div class={textContainerClass()}>
|
<div class={textContainerClass()}>
|
||||||
<Show
|
<Show
|
||||||
when={isAssistantMessage()}
|
when={isAssistantMessage()}
|
||||||
fallback={<span>{plainTextContent()}</span>}
|
fallback={<span class="text-primary">{plainTextContent()}</span>}
|
||||||
>
|
>
|
||||||
<Markdown
|
<Markdown
|
||||||
part={createTextPartForMarkdown()}
|
part={createTextPartForMarkdown()}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type { Model } from "../types/session"
|
|||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { preferences, toggleFavoriteModelPreference } from "../stores/preferences"
|
import { preferences, toggleFavoriteModelPreference } from "../stores/preferences"
|
||||||
import Kbd from "./kbd"
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
|
|
||||||
@@ -301,9 +300,6 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span class="selector-trigger-hint selector-trigger-hint--top" aria-hidden="true">
|
|
||||||
<Kbd shortcut="cmd+shift+m" />
|
|
||||||
</span>
|
|
||||||
<Combobox.Icon class="selector-trigger-icon">
|
<Combobox.Icon class="selector-trigger-icon">
|
||||||
<ChevronDown class="w-3 h-3" />
|
<ChevronDown class="w-3 h-3" />
|
||||||
</Combobox.Icon>
|
</Combobox.Icon>
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCl
|
|||||||
import type { SessionStatus } from "../types/session"
|
import type { SessionStatus } from "../types/session"
|
||||||
import type { SessionThread } from "../stores/session-state"
|
import type { SessionThread } from "../stores/session-state"
|
||||||
import { getSessionStatus } from "../stores/session-status"
|
import { getSessionStatus } from "../stores/session-status"
|
||||||
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid"
|
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown, Search, Square, CheckSquare, MinusSquare } from "lucide-solid"
|
||||||
import KeyboardHint from "./keyboard-hint"
|
import KeyboardHint from "./keyboard-hint"
|
||||||
import SessionRenameDialog from "./session-rename-dialog"
|
import SessionRenameDialog from "./session-rename-dialog"
|
||||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||||
import { showToastNotification } from "../lib/notifications"
|
import { showToastNotification } from "../lib/notifications"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import { showConfirmDialog } from "../stores/alerts"
|
||||||
import {
|
import {
|
||||||
deleteSession,
|
deleteSession,
|
||||||
ensureSessionParentExpanded,
|
ensureSessionParentExpanded,
|
||||||
@@ -35,6 +36,7 @@ interface SessionListProps {
|
|||||||
showFooter?: boolean
|
showFooter?: boolean
|
||||||
headerContent?: JSX.Element
|
headerContent?: JSX.Element
|
||||||
footerContent?: JSX.Element
|
footerContent?: JSX.Element
|
||||||
|
enableFilterBar?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSessionStatus(status: SessionStatus): string {
|
function formatSessionStatus(status: SessionStatus): string {
|
||||||
@@ -46,6 +48,70 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null)
|
const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null)
|
||||||
const [isRenaming, setIsRenaming] = createSignal(false)
|
const [isRenaming, setIsRenaming] = createSignal(false)
|
||||||
|
|
||||||
|
const [filterQuery, setFilterQuery] = createSignal("")
|
||||||
|
const normalizedQuery = createMemo(() => filterQuery().trim().toLowerCase())
|
||||||
|
|
||||||
|
const [selectedSessionIds, setSelectedSessionIds] = createSignal<Set<string>>(new Set())
|
||||||
|
|
||||||
|
const normalizeSessionLabel = (sessionId: string) => {
|
||||||
|
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||||
|
const title = (session?.title ?? "").trim()
|
||||||
|
return title || t("sessionList.session.untitled")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionMatchesQuery = (sessionId: string, query: string) => {
|
||||||
|
if (!query) return true
|
||||||
|
const label = normalizeSessionLabel(sessionId).toLowerCase()
|
||||||
|
if (label.includes(query)) return true
|
||||||
|
return sessionId.toLowerCase().includes(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredThreads = createMemo<SessionThread[]>(() => {
|
||||||
|
const query = normalizedQuery()
|
||||||
|
if (!query) return props.threads
|
||||||
|
|
||||||
|
const next: SessionThread[] = []
|
||||||
|
for (const thread of props.threads) {
|
||||||
|
const parentMatches = sessionMatchesQuery(thread.parent.id, query)
|
||||||
|
const matchingChildren = thread.children.filter((child) => sessionMatchesQuery(child.id, query))
|
||||||
|
|
||||||
|
if (!parentMatches && matchingChildren.length === 0) continue
|
||||||
|
|
||||||
|
next.push({
|
||||||
|
parent: thread.parent,
|
||||||
|
children: matchingChildren,
|
||||||
|
latestUpdated: thread.latestUpdated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
const allMatchingSessionIds = createMemo<string[]>(() => {
|
||||||
|
const ids: string[] = []
|
||||||
|
for (const thread of filteredThreads()) {
|
||||||
|
ids.push(thread.parent.id)
|
||||||
|
for (const child of thread.children) ids.push(child.id)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedCount = createMemo(() => selectedSessionIds().size)
|
||||||
|
|
||||||
|
const isAllSelected = createMemo(() => {
|
||||||
|
const ids = allMatchingSessionIds()
|
||||||
|
if (ids.length === 0) return false
|
||||||
|
const selected = selectedSessionIds()
|
||||||
|
return ids.every((id) => selected.has(id))
|
||||||
|
})
|
||||||
|
const isSelectAllIndeterminate = createMemo(() => {
|
||||||
|
const ids = allMatchingSessionIds()
|
||||||
|
const total = ids.length
|
||||||
|
if (total === 0) return false
|
||||||
|
const count = selectedCount()
|
||||||
|
return count > 0 && count < total
|
||||||
|
})
|
||||||
|
|
||||||
const isSessionDeleting = (sessionId: string) => {
|
const isSessionDeleting = (sessionId: string) => {
|
||||||
const deleting = loading().deletingSession.get(props.instanceId)
|
const deleting = loading().deletingSession.get(props.instanceId)
|
||||||
return deleting ? deleting.has(sessionId) : false
|
return deleting ? deleting.has(sessionId) : false
|
||||||
@@ -54,9 +120,10 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
|
|
||||||
const selectSession = (sessionId: string) => {
|
const selectSession = (sessionId: string) => {
|
||||||
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||||
const parentId = session?.parentId ?? session?.id
|
// If the user selects a child session, make sure its parent thread is expanded.
|
||||||
if (parentId) {
|
// For parent sessions we don't force expansion; user can collapse/expand freely.
|
||||||
ensureSessionParentExpanded(props.instanceId, parentId)
|
if (session?.parentId) {
|
||||||
|
ensureSessionParentExpanded(props.instanceId, session.parentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onSelect(sessionId)
|
props.onSelect(sessionId)
|
||||||
@@ -82,6 +149,17 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (isSessionDeleting(sessionId)) return
|
if (isSessionDeleting(sessionId)) return
|
||||||
|
|
||||||
|
const confirmed = await showConfirmDialog(
|
||||||
|
t("sessionList.delete.confirmMessage", { label: normalizeSessionLabel(sessionId) }),
|
||||||
|
{
|
||||||
|
title: t("sessionList.delete.title"),
|
||||||
|
variant: "warning",
|
||||||
|
confirmLabel: t("sessionList.delete.confirmLabel"),
|
||||||
|
cancelLabel: t("sessionList.delete.cancelLabel"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
const shouldSelectFallback = props.activeSessionId === sessionId
|
const shouldSelectFallback = props.activeSessionId === sessionId
|
||||||
let fallbackSessionId: string | undefined
|
let fallbackSessionId: string | undefined
|
||||||
|
|
||||||
@@ -153,6 +231,115 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setSelectedMany = (sessionIds: string[], checked: boolean) => {
|
||||||
|
if (sessionIds.length === 0) return
|
||||||
|
setSelectedSessionIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
sessionIds.forEach((id) => {
|
||||||
|
if (checked) next.add(id)
|
||||||
|
else next.delete(id)
|
||||||
|
})
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelectableThreadIds = (parentId: string): string[] => {
|
||||||
|
const query = normalizedQuery()
|
||||||
|
const source = query ? filteredThreads() : props.threads
|
||||||
|
const thread = source.find((t) => t.parent.id === parentId)
|
||||||
|
if (!thread) return [parentId]
|
||||||
|
return [thread.parent.id, ...thread.children.map((c) => c.id)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllSessionIdsInOrder = (threads: SessionThread[]): string[] => {
|
||||||
|
const ids: string[] = []
|
||||||
|
threads.forEach((thread) => {
|
||||||
|
ids.push(thread.parent.id)
|
||||||
|
thread.children.forEach((child) => ids.push(child.id))
|
||||||
|
})
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleSelectAll = (checked: boolean) => {
|
||||||
|
const ids = allMatchingSessionIds()
|
||||||
|
setSelectedMany(ids, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
if (isAllSelected()) {
|
||||||
|
handleToggleSelectAll(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleToggleSelectAll(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBulkDelete = async () => {
|
||||||
|
const selected = Array.from(selectedSessionIds())
|
||||||
|
if (selected.length === 0) return
|
||||||
|
|
||||||
|
const confirmed = await showConfirmDialog(
|
||||||
|
t("sessionList.bulkDelete.confirmMessage", { count: selected.length }),
|
||||||
|
{
|
||||||
|
title: t("sessionList.bulkDelete.title"),
|
||||||
|
variant: "warning",
|
||||||
|
confirmLabel: t("sessionList.bulkDelete.confirmLabel"),
|
||||||
|
cancelLabel: t("sessionList.bulkDelete.cancelLabel"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
const deletedSet = new Set(selected)
|
||||||
|
const currentActiveId = props.activeSessionId
|
||||||
|
|
||||||
|
let fallbackSessionId: string | undefined
|
||||||
|
if (currentActiveId && deletedSet.has(currentActiveId)) {
|
||||||
|
const ordered = getAllSessionIdsInOrder(props.threads)
|
||||||
|
const currentIndex = ordered.indexOf(currentActiveId)
|
||||||
|
|
||||||
|
for (let i = Math.max(0, currentIndex); i < ordered.length; i++) {
|
||||||
|
const candidate = ordered[i]
|
||||||
|
if (candidate && !deletedSet.has(candidate)) {
|
||||||
|
fallbackSessionId = candidate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!fallbackSessionId) {
|
||||||
|
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||||
|
const candidate = ordered[i]
|
||||||
|
if (candidate && !deletedSet.has(candidate)) {
|
||||||
|
fallbackSessionId = candidate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let failed = 0
|
||||||
|
for (const sessionId of selected) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await deleteSession(props.instanceId, sessionId)
|
||||||
|
} catch (error) {
|
||||||
|
failed += 1
|
||||||
|
log.error(`Failed to delete session ${sessionId}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedSessionIds(new Set<string>())
|
||||||
|
|
||||||
|
if (fallbackSessionId) {
|
||||||
|
setActiveSessionFromList(props.instanceId, fallbackSessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
showToastNotification({
|
||||||
|
message: t("sessionList.bulkDelete.error", { count: failed }),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const SessionRow: Component<{
|
const SessionRow: Component<{
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -190,9 +377,31 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
? t("sessionList.status.needsInput")
|
? t("sessionList.status.needsInput")
|
||||||
: statusLabel()
|
: statusLabel()
|
||||||
|
|
||||||
|
const isSelected = () => selectedSessionIds().has(rowProps.sessionId)
|
||||||
|
|
||||||
|
const parentGroupState = createMemo(() => {
|
||||||
|
if (rowProps.isChild) {
|
||||||
|
return { checked: isSelected(), indeterminate: false, ids: [rowProps.sessionId] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = getSelectableThreadIds(rowProps.sessionId)
|
||||||
|
const selected = selectedSessionIds()
|
||||||
|
const selectedInGroup = ids.reduce((count, id) => (selected.has(id) ? count + 1 : count), 0)
|
||||||
|
return {
|
||||||
|
checked: selectedInGroup > 0 && selectedInGroup === ids.length,
|
||||||
|
indeterminate: selectedInGroup > 0 && selectedInGroup < ids.length,
|
||||||
|
ids,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let rowCheckboxEl: HTMLInputElement | null = null
|
||||||
|
createEffect(() => {
|
||||||
|
if (!rowCheckboxEl) return
|
||||||
|
rowCheckboxEl.indeterminate = parentGroupState().indeterminate
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="session-list-item group">
|
<div class="session-list-item group">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class={`session-item-base ${rowProps.isChild ? `session-item-child${rowProps.isLastChild ? " session-item-child-last" : ""} session-item-border-assistant session-item-kind-assistant` : "session-item-border-user session-item-kind-user"} ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
class={`session-item-base ${rowProps.isChild ? `session-item-child${rowProps.isLastChild ? " session-item-child-last" : ""} session-item-border-assistant session-item-kind-assistant` : "session-item-border-user session-item-kind-user"} ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
||||||
data-session-id={rowProps.sessionId}
|
data-session-id={rowProps.sessionId}
|
||||||
@@ -204,11 +413,23 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<div class="session-item-row session-item-header">
|
<div class="session-item-row session-item-header">
|
||||||
<div class="session-item-title-row">
|
<div class="session-item-title-row">
|
||||||
{rowProps.isChild ? (
|
<Show when={props.enableFilterBar}>
|
||||||
<Bot class="w-4 h-4 flex-shrink-0" />
|
<input
|
||||||
) : (
|
ref={(el) => {
|
||||||
<User class="w-4 h-4 flex-shrink-0" />
|
rowCheckboxEl = el
|
||||||
)}
|
}}
|
||||||
|
type="checkbox"
|
||||||
|
checked={parentGroupState().checked}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onChange={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setSelectedMany(parentGroupState().ids, event.currentTarget.checked)
|
||||||
|
}}
|
||||||
|
aria-label={t("sessionList.selection.checkboxAriaLabel")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{rowProps.isChild ? <Bot class="w-4 h-4 flex-shrink-0" /> : <User class="w-4 h-4 flex-shrink-0" />}
|
||||||
<span class="session-item-title session-item-title--clamp">{title()}</span>
|
<span class="session-item-title session-item-title--clamp">{title()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,9 +437,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<Show
|
<Show
|
||||||
when={rowProps.hasChildren && !rowProps.isChild}
|
when={rowProps.hasChildren && !rowProps.isChild}
|
||||||
fallback={
|
fallback={rowProps.isChild ? null : <span class="session-item-expander session-item-expander--spacer" aria-hidden="true" />}
|
||||||
rowProps.isChild ? null : <span class="session-item-expander session-item-expander--spacer" aria-hidden="true" />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class={`session-item-expander opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
class={`session-item-expander opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
||||||
@@ -228,18 +447,16 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={rowProps.expanded ? t("sessionList.expand.collapseAriaLabel") : t("sessionList.expand.expandAriaLabel")}
|
aria-label={
|
||||||
|
rowProps.expanded ? t("sessionList.expand.collapseAriaLabel") : t("sessionList.expand.expandAriaLabel")
|
||||||
|
}
|
||||||
title={rowProps.expanded ? t("sessionList.expand.collapseTitle") : t("sessionList.expand.expandTitle")}
|
title={rowProps.expanded ? t("sessionList.expand.collapseTitle") : t("sessionList.expand.expandTitle")}
|
||||||
>
|
>
|
||||||
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
<span class={`status-indicator session-status session-status-list ${statusClassName()}`}>
|
<span class={`status-indicator session-status session-status-list ${statusClassName()}`}>
|
||||||
{needsInput() ? (
|
{needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||||
<ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<span class="status-dot" />
|
|
||||||
)}
|
|
||||||
{statusText()}
|
{statusText()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -309,6 +526,13 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
// Keep the active child session visible by ensuring its parent is expanded.
|
||||||
|
// Don't force-expanding when the active session itself is a parent lets users collapse it.
|
||||||
|
const activeId = props.activeSessionId
|
||||||
|
if (!activeId || activeId === "info") return
|
||||||
|
const activeSession = sessionStateSessions().get(props.instanceId)?.get(activeId)
|
||||||
|
if (!activeSession) return
|
||||||
|
if (!activeSession.parentId) return
|
||||||
const parentId = activeParentId()
|
const parentId = activeParentId()
|
||||||
if (!parentId) return
|
if (!parentId) return
|
||||||
ensureSessionParentExpanded(props.instanceId, parentId)
|
ensureSessionParentExpanded(props.instanceId, parentId)
|
||||||
@@ -365,6 +589,63 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<div
|
<div
|
||||||
class="session-list-container bg-surface-secondary border-r border-base flex flex-col w-full"
|
class="session-list-container bg-surface-secondary border-r border-base flex flex-col w-full"
|
||||||
>
|
>
|
||||||
|
<Show when={props.enableFilterBar}>
|
||||||
|
<div class="p-3 border-b border-base">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="relative flex-1 min-w-0">
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-muted" aria-hidden="true">
|
||||||
|
<Search class="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input pl-9"
|
||||||
|
value={filterQuery()}
|
||||||
|
onInput={(e) => setFilterQuery(e.currentTarget.value)}
|
||||||
|
placeholder={t("sessionList.filter.placeholder")}
|
||||||
|
aria-label={t("sessionList.filter.ariaLabel")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button-tertiary p-2 inline-flex items-center justify-center"
|
||||||
|
onClick={toggleSelectAll}
|
||||||
|
disabled={allMatchingSessionIds().length === 0}
|
||||||
|
aria-label={t("sessionList.selection.selectAllAriaLabel")}
|
||||||
|
title={t("sessionList.selection.selectAllLabel")}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={isSelectAllIndeterminate()}
|
||||||
|
fallback={isAllSelected() ? <CheckSquare class="w-4 h-4" /> : <Square class="w-4 h-4" />}
|
||||||
|
>
|
||||||
|
<MinusSquare class="w-4 h-4" />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={selectedCount() > 0}>
|
||||||
|
<div class="mt-2 flex items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button-tertiary"
|
||||||
|
onClick={handleBulkDelete}
|
||||||
|
aria-label={t("sessionList.bulkDelete.ariaLabel", { count: selectedCount() })}
|
||||||
|
>
|
||||||
|
{t("sessionList.bulkDelete.button", { count: selectedCount() })}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button-tertiary"
|
||||||
|
onClick={() => setSelectedSessionIds(new Set<string>())}
|
||||||
|
aria-label={t("sessionList.selection.clearAriaLabel")}
|
||||||
|
>
|
||||||
|
{t("sessionList.selection.clearLabel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={props.showHeader !== false}>
|
<Show when={props.showHeader !== false}>
|
||||||
<div class="session-list-header p-3 border-b border-base">
|
<div class="session-list-header p-3 border-b border-base">
|
||||||
{props.headerContent ?? (
|
{props.headerContent ?? (
|
||||||
@@ -380,12 +661,12 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
|
|
||||||
<div class="session-list flex-1 overflow-y-auto" ref={(el) => listEl[1](el)}>
|
<div class="session-list flex-1 overflow-y-auto" ref={(el) => listEl[1](el)}>
|
||||||
|
|
||||||
<Show when={props.threads.length > 0}>
|
<Show when={filteredThreads().length > 0}>
|
||||||
<div class="session-section">
|
<div class="session-section">
|
||||||
<For each={props.threads}>
|
<For each={filteredThreads()}>
|
||||||
|
|
||||||
{(thread) => {
|
{(thread) => {
|
||||||
const expanded = () => isSessionParentExpanded(props.instanceId, thread.parent.id)
|
const expanded = () => (normalizedQuery() ? true : isSessionParentExpanded(props.instanceId, thread.parent.id))
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SessionRow
|
<SessionRow
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ interface ContextUsagePanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chipClass = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary"
|
const chipClass = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary"
|
||||||
const chipLabelClass = "uppercase text-[10px] tracking-wide text-primary/70"
|
const chipLabelClass = "uppercase text-[10px] tracking-wide text-muted"
|
||||||
const headingClass = "text-xs font-semibold text-primary/70 uppercase tracking-wide"
|
const headingClass = "text-xs font-semibold text-muted uppercase tracking-wide"
|
||||||
|
|
||||||
const ContextUsagePanel: Component<ContextUsagePanelProps> = (props) => {
|
const ContextUsagePanel: Component<ContextUsagePanelProps> = (props) => {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -49,7 +49,7 @@ const ContextUsagePanel: Component<ContextUsagePanelProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="session-context-panel border-r border-base border-b px-3 py-3 space-y-3">
|
<div class="session-context-panel border-r border-base border-b px-3 py-3 space-y-3">
|
||||||
<div class="flex flex-wrap items-center gap-2 text-xs text-primary/90">
|
<div class="flex flex-wrap items-center gap-2 text-xs text-primary">
|
||||||
<div class={headingClass}>{t("contextUsagePanel.headings.tokens")}</div>
|
<div class={headingClass}>{t("contextUsagePanel.headings.tokens")}</div>
|
||||||
<div class={chipClass}>
|
<div class={chipClass}>
|
||||||
<span class={chipLabelClass}>{t("contextUsagePanel.labels.input")}</span>
|
<span class={chipLabelClass}>{t("contextUsagePanel.labels.input")}</span>
|
||||||
@@ -65,7 +65,7 @@ const ContextUsagePanel: Component<ContextUsagePanelProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 text-xs text-primary/90">
|
<div class="flex flex-wrap items-center gap-2 text-xs text-primary">
|
||||||
<div class={headingClass}>{t("contextUsagePanel.headings.context")}</div>
|
<div class={headingClass}>{t("contextUsagePanel.headings.context")}</div>
|
||||||
<div class={chipClass}>
|
<div class={chipClass}>
|
||||||
<span class={chipLabelClass}>{t("contextUsagePanel.labels.used")}</span>
|
<span class={chipLabelClass}>{t("contextUsagePanel.labels.used")}</span>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import PromptInput from "../prompt-input"
|
|||||||
import type { Attachment as PromptAttachment } from "../../types/attachment"
|
import type { Attachment as PromptAttachment } from "../../types/attachment"
|
||||||
import { getAttachments, removeAttachment } from "../../stores/attachments"
|
import { getAttachments, removeAttachment } from "../../stores/attachments"
|
||||||
import { instances } from "../../stores/instances"
|
import { instances } from "../../stores/instances"
|
||||||
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
|
import { loadMessages, sendMessage, forkSession, renameSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
|
||||||
import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-status"
|
import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-status"
|
||||||
import { showAlertDialog } from "../../stores/alerts"
|
import { showAlertDialog } from "../../stores/alerts"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
@@ -217,10 +217,15 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const restoredText = getUserMessageText(messageId)
|
const restoredText = getUserMessageText(messageId)
|
||||||
|
const parentTitle = (session()?.title ?? "").trim() || t("sessionList.session.untitled")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const forkedSession = await forkSession(props.instanceId, props.sessionId, { messageId })
|
const forkedSession = await forkSession(props.instanceId, props.sessionId, { messageId })
|
||||||
|
|
||||||
|
renameSession(props.instanceId, forkedSession.id, `Fork: ${parentTitle}`).catch((error) => {
|
||||||
|
log.error("Failed to rename forked session", error)
|
||||||
|
})
|
||||||
|
|
||||||
const parentToActivate = forkedSession.parentId ?? forkedSession.id
|
const parentToActivate = forkedSession.parentId ?? forkedSession.id
|
||||||
setActiveParentSession(props.instanceId, parentToActivate)
|
setActiveParentSession(props.instanceId, parentToActivate)
|
||||||
if (forkedSession.parentId) {
|
if (forkedSession.parentId) {
|
||||||
|
|||||||
39
packages/ui/src/components/theme-mode-toggle.tsx
Normal file
39
packages/ui/src/components/theme-mode-toggle.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { createMemo, type Component } from "solid-js"
|
||||||
|
import { Laptop, Moon, Sun } from "lucide-solid"
|
||||||
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import { useTheme } from "../lib/theme"
|
||||||
|
|
||||||
|
interface ThemeModeToggleProps {
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeModeToggle: Component<ThemeModeToggleProps> = (props) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { themeMode, cycleThemeMode } = useTheme()
|
||||||
|
|
||||||
|
const modeLabel = () => {
|
||||||
|
const mode = themeMode()
|
||||||
|
if (mode === "system") return t("theme.mode.system")
|
||||||
|
if (mode === "light") return t("theme.mode.light")
|
||||||
|
return t("theme.mode.dark")
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = createMemo(() => {
|
||||||
|
const mode = themeMode()
|
||||||
|
if (mode === "system") return <Laptop class="w-4 h-4" />
|
||||||
|
if (mode === "light") return <Sun class="w-4 h-4" />
|
||||||
|
return <Moon class="w-4 h-4" />
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={props.class ?? "new-tab-button"}
|
||||||
|
onClick={cycleThemeMode}
|
||||||
|
aria-label={t("theme.toggle.ariaLabel", { mode: modeLabel() })}
|
||||||
|
title={t("theme.toggle.title", { mode: modeLabel() })}
|
||||||
|
>
|
||||||
|
{icon()}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import { ChevronDown } from "lucide-solid"
|
|||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { getModelThinkingSelection, setModelThinkingSelection } from "../stores/preferences"
|
import { getModelThinkingSelection, setModelThinkingSelection } from "../stores/preferences"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
import Kbd from "./kbd"
|
|
||||||
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
@@ -93,9 +92,6 @@ export default function ThinkingSelector(props: ThinkingSelectorProps) {
|
|||||||
<div class="selector-trigger-label selector-trigger-label--stacked flex-1 min-w-0">
|
<div class="selector-trigger-label selector-trigger-label--stacked flex-1 min-w-0">
|
||||||
<span class="selector-trigger-primary selector-trigger-primary--align-left">{triggerPrimary()}</span>
|
<span class="selector-trigger-primary selector-trigger-primary--align-left">{triggerPrimary()}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="selector-trigger-hint" aria-hidden="true">
|
|
||||||
<Kbd shortcut="cmd+shift+t" />
|
|
||||||
</span>
|
|
||||||
<Combobox.Icon class="selector-trigger-icon">
|
<Combobox.Icon class="selector-trigger-icon">
|
||||||
<ChevronDown class="w-3 h-3" />
|
<ChevronDown class="w-3 h-3" />
|
||||||
</Combobox.Icon>
|
</Combobox.Icon>
|
||||||
|
|||||||
@@ -235,12 +235,16 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
restoreScrollPosition(autoScroll())
|
restoreScrollPosition(autoScroll())
|
||||||
if (!expanded()) return
|
if (!expanded()) return
|
||||||
scheduleAnchorScroll()
|
scheduleAnchorScroll(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const initializeScrollContainer = (element: HTMLDivElement | null | undefined) => {
|
const initializeScrollContainer = (element: HTMLDivElement | null | undefined) => {
|
||||||
scrollContainerRef = element || undefined
|
const next = element || undefined
|
||||||
|
if (next === scrollContainerRef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scrollContainerRef = next
|
||||||
setScrollContainer(scrollContainerRef)
|
setScrollContainer(scrollContainerRef)
|
||||||
if (scrollContainerRef) {
|
if (scrollContainerRef) {
|
||||||
restoreScrollPosition(autoScroll())
|
restoreScrollPosition(autoScroll())
|
||||||
@@ -593,7 +597,7 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
previousPartVersion = version
|
previousPartVersion = version
|
||||||
scheduleAnchorScroll()
|
scheduleAnchorScroll(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function createAnsiContentRenderer(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={messageClass} ref={(element) => params.scrollHelpers.registerContainer(element)} onScroll={params.scrollHelpers.handleScroll}>
|
<div class={messageClass} ref={params.scrollHelpers.registerContainer} onScroll={params.scrollHelpers.handleScroll}>
|
||||||
<pre class="tool-call-content tool-call-ansi" innerHTML={nextCache.html} />
|
<pre class="tool-call-content tool-call-ansi" innerHTML={nextCache.html} />
|
||||||
{params.scrollHelpers.renderSentinel()}
|
{params.scrollHelpers.renderSentinel()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ export function createDiffContentRenderer(params: {
|
|||||||
handleScrollRendered: () => void
|
handleScrollRendered: () => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const registerTracked = (element: HTMLDivElement | null) => {
|
||||||
|
params.scrollHelpers.registerContainer(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerUntracked = (element: HTMLDivElement | null) => {
|
||||||
|
params.scrollHelpers.registerContainer(element, { disableTracking: true })
|
||||||
|
}
|
||||||
|
|
||||||
function renderDiffContent(payload: DiffPayload, options?: DiffRenderOptions): JSXElement | null {
|
function renderDiffContent(payload: DiffPayload, options?: DiffRenderOptions): JSXElement | null {
|
||||||
const relativePath = payload.filePath ? getRelativePath(payload.filePath) : ""
|
const relativePath = payload.filePath ? getRelativePath(payload.filePath) : ""
|
||||||
const toolbarLabel = options?.label || (relativePath
|
const toolbarLabel = options?.label || (relativePath
|
||||||
@@ -35,6 +43,8 @@ export function createDiffContentRenderer(params: {
|
|||||||
const cacheHandle = selectedVariant === "permission-diff" ? params.permissionDiffCache : params.diffCache
|
const cacheHandle = selectedVariant === "permission-diff" ? params.permissionDiffCache : params.diffCache
|
||||||
const diffMode = () => (params.preferences().diffViewMode || "split") as DiffViewMode
|
const diffMode = () => (params.preferences().diffViewMode || "split") as DiffViewMode
|
||||||
const themeKey = params.isDark() ? "dark" : "light"
|
const themeKey = params.isDark() ? "dark" : "light"
|
||||||
|
const disableScrollTracking = Boolean(options?.disableScrollTracking)
|
||||||
|
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||||
|
|
||||||
const baseEntryParams = cacheHandle.params() as any
|
const baseEntryParams = cacheHandle.params() as any
|
||||||
const cacheEntryParams = (() => {
|
const cacheEntryParams = (() => {
|
||||||
@@ -58,7 +68,7 @@ export function createDiffContentRenderer(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDiffRendered = () => {
|
const handleDiffRendered = () => {
|
||||||
if (!options?.disableScrollTracking) {
|
if (!disableScrollTracking) {
|
||||||
params.handleScrollRendered()
|
params.handleScrollRendered()
|
||||||
}
|
}
|
||||||
params.onContentRendered?.()
|
params.onContentRendered?.()
|
||||||
@@ -67,8 +77,8 @@ export function createDiffContentRenderer(params: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="message-text tool-call-markdown tool-call-markdown-large tool-call-diff-shell"
|
class="message-text tool-call-markdown tool-call-markdown-large tool-call-diff-shell"
|
||||||
ref={(element) => params.scrollHelpers.registerContainer(element, { disableTracking: options?.disableScrollTracking })}
|
ref={registerRef}
|
||||||
onScroll={options?.disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
||||||
>
|
>
|
||||||
<div class="tool-call-diff-toolbar" role="group" aria-label={params.t("toolCall.diff.viewMode.ariaLabel")}>
|
<div class="tool-call-diff-toolbar" role="group" aria-label={params.t("toolCall.diff.viewMode.ariaLabel")}>
|
||||||
<span class="tool-call-diff-toolbar-label">{toolbarLabel}</span>
|
<span class="tool-call-diff-toolbar-label">{toolbarLabel}</span>
|
||||||
@@ -100,7 +110,7 @@ export function createDiffContentRenderer(params: {
|
|||||||
cacheEntryParams={cacheEntryParams as any}
|
cacheEntryParams={cacheEntryParams as any}
|
||||||
onRendered={handleDiffRendered}
|
onRendered={handleDiffRendered}
|
||||||
/>
|
/>
|
||||||
{params.scrollHelpers.renderSentinel({ disableTracking: options?.disableScrollTracking })}
|
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ export function createMarkdownContentRenderer(params: {
|
|||||||
handleScrollRendered: () => void
|
handleScrollRendered: () => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const registerTracked = (element: HTMLDivElement | null) => {
|
||||||
|
params.scrollHelpers.registerContainer(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerUntracked = (element: HTMLDivElement | null) => {
|
||||||
|
params.scrollHelpers.registerContainer(element, { disableTracking: true })
|
||||||
|
}
|
||||||
|
|
||||||
function renderMarkdownContent(options: MarkdownRenderOptions): JSXElement | null {
|
function renderMarkdownContent(options: MarkdownRenderOptions): JSXElement | null {
|
||||||
if (!options.content) {
|
if (!options.content) {
|
||||||
return null
|
return null
|
||||||
@@ -24,6 +32,7 @@ export function createMarkdownContentRenderer(params: {
|
|||||||
const disableHighlight = options.disableHighlight || false
|
const disableHighlight = options.disableHighlight || false
|
||||||
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}`
|
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}`
|
||||||
const disableScrollTracking = options.disableScrollTracking || false
|
const disableScrollTracking = options.disableScrollTracking || false
|
||||||
|
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||||
|
|
||||||
const state = params.toolState()
|
const state = params.toolState()
|
||||||
const shouldDeferMarkdown = Boolean(state && (state.status === "running" || state.status === "pending") && disableHighlight)
|
const shouldDeferMarkdown = Boolean(state && (state.status === "running" || state.status === "pending") && disableHighlight)
|
||||||
@@ -31,7 +40,7 @@ export function createMarkdownContentRenderer(params: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={messageClass}
|
class={messageClass}
|
||||||
ref={(element) => params.scrollHelpers.registerContainer(element, { disableTracking: disableScrollTracking })}
|
ref={registerRef}
|
||||||
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
||||||
>
|
>
|
||||||
<pre class="whitespace-pre-wrap break-words text-sm font-mono">{options.content}</pre>
|
<pre class="whitespace-pre-wrap break-words text-sm font-mono">{options.content}</pre>
|
||||||
@@ -56,7 +65,7 @@ export function createMarkdownContentRenderer(params: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={messageClass}
|
class={messageClass}
|
||||||
ref={(element) => params.scrollHelpers.registerContainer(element, { disableTracking: disableScrollTracking })}
|
ref={registerRef}
|
||||||
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
||||||
>
|
>
|
||||||
<Markdown
|
<Markdown
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createMemo, Show, For, type Accessor } from "solid-js"
|
import { createMemo, Show, For, createEffect, type Accessor } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk"
|
||||||
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { useI18n } from "../../lib/i18n"
|
import { useI18n } from "../../lib/i18n"
|
||||||
@@ -28,6 +28,13 @@ export type QuestionToolBlockProps = {
|
|||||||
|
|
||||||
export function QuestionToolBlock(props: QuestionToolBlockProps) {
|
export function QuestionToolBlock(props: QuestionToolBlockProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
let firstInputRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.active() && firstInputRef) {
|
||||||
|
firstInputRef.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const requestId = createMemo(() => {
|
const requestId = createMemo(() => {
|
||||||
const state = props.toolState()
|
const state = props.toolState()
|
||||||
@@ -206,7 +213,7 @@ export function QuestionToolBlock(props: QuestionToolBlockProps) {
|
|||||||
|
|
||||||
<div class="mt-3 flex flex-col gap-1">
|
<div class="mt-3 flex flex-col gap-1">
|
||||||
<For each={q?.options ?? []}>
|
<For each={q?.options ?? []}>
|
||||||
{(opt) => {
|
{(opt, optIndex) => {
|
||||||
const checked = () => selected().includes(opt.label)
|
const checked = () => selected().includes(opt.label)
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
@@ -214,6 +221,9 @@ export function QuestionToolBlock(props: QuestionToolBlockProps) {
|
|||||||
title={opt.description}
|
title={opt.description}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
ref={(el) => {
|
||||||
|
if (i() === 0 && optIndex() === 0) firstInputRef = el
|
||||||
|
}}
|
||||||
type={inputType()}
|
type={inputType()}
|
||||||
name={groupName()}
|
name={groupName()}
|
||||||
checked={checked()}
|
checked={checked()}
|
||||||
@@ -234,6 +244,9 @@ export function QuestionToolBlock(props: QuestionToolBlockProps) {
|
|||||||
title={t("toolCall.question.custom.title")}
|
title={t("toolCall.question.custom.title")}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
ref={(el) => {
|
||||||
|
if (i() === 0 && (q?.options?.length ?? 0) === 0) firstInputRef = el
|
||||||
|
}}
|
||||||
type={inputType()}
|
type={inputType()}
|
||||||
name={groupName()}
|
name={groupName()}
|
||||||
checked={customChecked()}
|
checked={customChecked()}
|
||||||
@@ -266,6 +279,13 @@ export function QuestionToolBlock(props: QuestionToolBlockProps) {
|
|||||||
toggleFromCustomInput(i(), e.currentTarget)
|
toggleFromCustomInput(i(), e.currentTarget)
|
||||||
}}
|
}}
|
||||||
onInput={(e) => handleCustomTyping(i(), e.currentTarget)}
|
onInput={(e) => handleCustomTyping(i(), e.currentTarget)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && !e.isComposing) {
|
||||||
|
if (!submitDisabled()) {
|
||||||
|
props.onSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export const taskRenderer: ToolRenderer = {
|
|||||||
<div class="tool-call-task-section-body">
|
<div class="tool-call-task-section-body">
|
||||||
<div
|
<div
|
||||||
class="message-text tool-call-markdown tool-call-task-container"
|
class="message-text tool-call-markdown tool-call-task-container"
|
||||||
ref={(element) => scrollHelpers?.registerContainer(element)}
|
ref={scrollHelpers?.registerContainer}
|
||||||
onScroll={
|
onScroll={
|
||||||
scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined
|
scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,4 +29,10 @@ export const appMessages = {
|
|||||||
|
|
||||||
"releases.uiUpdated.title": "UI updated",
|
"releases.uiUpdated.title": "UI updated",
|
||||||
"releases.uiUpdated.message": "UI is now updated to {version}.",
|
"releases.uiUpdated.message": "UI is now updated to {version}.",
|
||||||
|
|
||||||
|
"theme.mode.system": "System",
|
||||||
|
"theme.mode.light": "Light",
|
||||||
|
"theme.mode.dark": "Dark",
|
||||||
|
"theme.toggle.title": "Theme: {mode}",
|
||||||
|
"theme.toggle.ariaLabel": "Theme: {mode}",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Go to Session",
|
"messageBlock.tool.goToSession.label": "Go to Session",
|
||||||
"messageBlock.tool.goToSession.title": "Go to session",
|
"messageBlock.tool.goToSession.title": "Go to session",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "Session not available yet",
|
"messageBlock.tool.goToSession.unavailableTitle": "Session not available yet",
|
||||||
|
"messageBlock.tool.deletePart.label": "Delete",
|
||||||
|
"messageBlock.tool.deletePart.deleting": "Deleting...",
|
||||||
|
"messageBlock.tool.deletePart.title": "Delete this tool call output",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "Delete failed",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "Failed to delete tool call output",
|
||||||
|
|
||||||
"messageBlock.compaction.ariaLabel": "Session compaction",
|
"messageBlock.compaction.ariaLabel": "Session compaction",
|
||||||
"messageBlock.compaction.autoLabel": "Session auto-compacted",
|
"messageBlock.compaction.autoLabel": "Session auto-compacted",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"messageItem.status.generating": "Generating...",
|
"messageItem.status.generating": "Generating...",
|
||||||
"messageItem.status.sending": "Sending...",
|
"messageItem.status.sending": "Sending...",
|
||||||
"messageItem.status.failedToSend": "Message failed to send",
|
"messageItem.status.failedToSend": "Message failed to send",
|
||||||
|
"messagePart.actions.delete": "Delete",
|
||||||
|
"messagePart.actions.deleting": "Deleting...",
|
||||||
|
"messagePart.actions.deleteTitle": "Delete this item",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "Delete failed",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "Failed to delete item",
|
||||||
"messageItem.attachment.defaultName": "attachment",
|
"messageItem.attachment.defaultName": "attachment",
|
||||||
"messageItem.attachment.downloadAriaLabel": "Download {name}",
|
"messageItem.attachment.downloadAriaLabel": "Download {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "Agent: {agent}",
|
"messageItem.agentMeta.agentLabel": "Agent: {agent}",
|
||||||
|
|||||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
|||||||
"sessionList.copyId.success": "Session ID copied",
|
"sessionList.copyId.success": "Session ID copied",
|
||||||
"sessionList.copyId.error": "Unable to copy session ID",
|
"sessionList.copyId.error": "Unable to copy session ID",
|
||||||
"sessionList.delete.error": "Unable to delete session",
|
"sessionList.delete.error": "Unable to delete session",
|
||||||
|
"sessionList.delete.title": "Delete session",
|
||||||
|
"sessionList.delete.confirmMessage": "Delete \"{label}\"? This cannot be undone.",
|
||||||
|
"sessionList.delete.confirmLabel": "Delete",
|
||||||
|
"sessionList.delete.cancelLabel": "Cancel",
|
||||||
"sessionList.rename.error": "Unable to rename session",
|
"sessionList.rename.error": "Unable to rename session",
|
||||||
|
|
||||||
|
"sessionList.filter.placeholder": "Search sessions…",
|
||||||
|
"sessionList.filter.ariaLabel": "Search sessions",
|
||||||
|
"sessionList.selection.selectAllLabel": "Select all",
|
||||||
|
"sessionList.selection.selectAllAriaLabel": "Select all sessions",
|
||||||
|
"sessionList.selection.clearLabel": "Clear",
|
||||||
|
"sessionList.selection.clearAriaLabel": "Clear selection",
|
||||||
|
"sessionList.selection.checkboxAriaLabel": "Select session",
|
||||||
|
"sessionList.bulkDelete.button": "Delete {count}",
|
||||||
|
"sessionList.bulkDelete.ariaLabel": "Delete {count} selected sessions",
|
||||||
|
"sessionList.bulkDelete.title": "Delete sessions",
|
||||||
|
"sessionList.bulkDelete.confirmMessage": "Delete {count} selected sessions? This cannot be undone.",
|
||||||
|
"sessionList.bulkDelete.confirmLabel": "Delete",
|
||||||
|
"sessionList.bulkDelete.cancelLabel": "Cancel",
|
||||||
|
"sessionList.bulkDelete.error": "Unable to delete {count} sessions",
|
||||||
|
|
||||||
"sessionRenameDialog.title": "Rename Session",
|
"sessionRenameDialog.title": "Rename Session",
|
||||||
"sessionRenameDialog.description.withLabel": "Update the title for \"{label}\".",
|
"sessionRenameDialog.description.withLabel": "Update the title for \"{label}\".",
|
||||||
"sessionRenameDialog.description.default": "Set a new title for this session.",
|
"sessionRenameDialog.description.default": "Set a new title for this session.",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Ir a sesión",
|
"messageBlock.tool.goToSession.label": "Ir a sesión",
|
||||||
"messageBlock.tool.goToSession.title": "Ir a la sesión",
|
"messageBlock.tool.goToSession.title": "Ir a la sesión",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "La sesión aún no está disponible",
|
"messageBlock.tool.goToSession.unavailableTitle": "La sesión aún no está disponible",
|
||||||
|
"messageBlock.tool.deletePart.label": "Eliminar",
|
||||||
|
"messageBlock.tool.deletePart.deleting": "Eliminando...",
|
||||||
|
"messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "Error al eliminar",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "No se pudo eliminar la salida de herramienta",
|
||||||
|
|
||||||
"messageBlock.compaction.ariaLabel": "Compactación de sesión",
|
"messageBlock.compaction.ariaLabel": "Compactación de sesión",
|
||||||
"messageBlock.compaction.autoLabel": "Sesión compactada automáticamente",
|
"messageBlock.compaction.autoLabel": "Sesión compactada automáticamente",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"messageItem.status.generating": "Generando...",
|
"messageItem.status.generating": "Generando...",
|
||||||
"messageItem.status.sending": "Enviando...",
|
"messageItem.status.sending": "Enviando...",
|
||||||
"messageItem.status.failedToSend": "No se pudo enviar el mensaje",
|
"messageItem.status.failedToSend": "No se pudo enviar el mensaje",
|
||||||
|
"messagePart.actions.delete": "Eliminar",
|
||||||
|
"messagePart.actions.deleting": "Eliminando...",
|
||||||
|
"messagePart.actions.deleteTitle": "Eliminar este elemento",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "Error al eliminar",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "No se pudo eliminar el elemento",
|
||||||
"messageItem.attachment.defaultName": "adjunto",
|
"messageItem.attachment.defaultName": "adjunto",
|
||||||
"messageItem.attachment.downloadAriaLabel": "Descargar {name}",
|
"messageItem.attachment.downloadAriaLabel": "Descargar {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "Agente: {agent}",
|
"messageItem.agentMeta.agentLabel": "Agente: {agent}",
|
||||||
|
|||||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
|||||||
"sessionList.copyId.success": "ID de sesión copiado",
|
"sessionList.copyId.success": "ID de sesión copiado",
|
||||||
"sessionList.copyId.error": "No se pudo copiar el ID de sesión",
|
"sessionList.copyId.error": "No se pudo copiar el ID de sesión",
|
||||||
"sessionList.delete.error": "No se pudo eliminar la sesión",
|
"sessionList.delete.error": "No se pudo eliminar la sesión",
|
||||||
|
"sessionList.delete.title": "Eliminar sesión",
|
||||||
|
"sessionList.delete.confirmMessage": "¿Eliminar \"{label}\"? Esto no se puede deshacer.",
|
||||||
|
"sessionList.delete.confirmLabel": "Eliminar",
|
||||||
|
"sessionList.delete.cancelLabel": "Cancelar",
|
||||||
"sessionList.rename.error": "No se pudo renombrar la sesión",
|
"sessionList.rename.error": "No se pudo renombrar la sesión",
|
||||||
|
|
||||||
|
"sessionList.filter.placeholder": "Buscar sesiones…",
|
||||||
|
"sessionList.filter.ariaLabel": "Buscar sesiones",
|
||||||
|
"sessionList.selection.selectAllLabel": "Seleccionar todo",
|
||||||
|
"sessionList.selection.selectAllAriaLabel": "Seleccionar todas las sesiones",
|
||||||
|
"sessionList.selection.clearLabel": "Limpiar",
|
||||||
|
"sessionList.selection.clearAriaLabel": "Limpiar selección",
|
||||||
|
"sessionList.selection.checkboxAriaLabel": "Seleccionar sesión",
|
||||||
|
"sessionList.bulkDelete.button": "Eliminar {count}",
|
||||||
|
"sessionList.bulkDelete.ariaLabel": "Eliminar {count} sesiones seleccionadas",
|
||||||
|
"sessionList.bulkDelete.title": "Eliminar sesiones",
|
||||||
|
"sessionList.bulkDelete.confirmMessage": "¿Eliminar {count} sesiones seleccionadas? Esto no se puede deshacer.",
|
||||||
|
"sessionList.bulkDelete.confirmLabel": "Eliminar",
|
||||||
|
"sessionList.bulkDelete.cancelLabel": "Cancelar",
|
||||||
|
"sessionList.bulkDelete.error": "No se pudieron eliminar {count} sesiones",
|
||||||
|
|
||||||
"sessionRenameDialog.title": "Renombrar sesión",
|
"sessionRenameDialog.title": "Renombrar sesión",
|
||||||
"sessionRenameDialog.description.withLabel": "Actualiza el título de \"{label}\".",
|
"sessionRenameDialog.description.withLabel": "Actualiza el título de \"{label}\".",
|
||||||
"sessionRenameDialog.description.default": "Establece un nuevo título para esta sesión.",
|
"sessionRenameDialog.description.default": "Establece un nuevo título para esta sesión.",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Aller à la session",
|
"messageBlock.tool.goToSession.label": "Aller à la session",
|
||||||
"messageBlock.tool.goToSession.title": "Aller à la session",
|
"messageBlock.tool.goToSession.title": "Aller à la session",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "Session pas encore disponible",
|
"messageBlock.tool.goToSession.unavailableTitle": "Session pas encore disponible",
|
||||||
|
"messageBlock.tool.deletePart.label": "Supprimer",
|
||||||
|
"messageBlock.tool.deletePart.deleting": "Suppression...",
|
||||||
|
"messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "Échec de suppression",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "Impossible de supprimer la sortie d'outil",
|
||||||
|
|
||||||
"messageBlock.compaction.ariaLabel": "Compaction de la session",
|
"messageBlock.compaction.ariaLabel": "Compaction de la session",
|
||||||
"messageBlock.compaction.autoLabel": "Session compactée automatiquement",
|
"messageBlock.compaction.autoLabel": "Session compactée automatiquement",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"messageItem.status.generating": "Génération...",
|
"messageItem.status.generating": "Génération...",
|
||||||
"messageItem.status.sending": "Envoi...",
|
"messageItem.status.sending": "Envoi...",
|
||||||
"messageItem.status.failedToSend": "Échec de l'envoi du message",
|
"messageItem.status.failedToSend": "Échec de l'envoi du message",
|
||||||
|
"messagePart.actions.delete": "Supprimer",
|
||||||
|
"messagePart.actions.deleting": "Suppression...",
|
||||||
|
"messagePart.actions.deleteTitle": "Supprimer cet élément",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "Échec de suppression",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "Impossible de supprimer l'élément",
|
||||||
"messageItem.attachment.defaultName": "piece-jointe",
|
"messageItem.attachment.defaultName": "piece-jointe",
|
||||||
"messageItem.attachment.downloadAriaLabel": "Télécharger {name}",
|
"messageItem.attachment.downloadAriaLabel": "Télécharger {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "Agent : {agent}",
|
"messageItem.agentMeta.agentLabel": "Agent : {agent}",
|
||||||
|
|||||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
|||||||
"sessionList.copyId.success": "ID de session copié",
|
"sessionList.copyId.success": "ID de session copié",
|
||||||
"sessionList.copyId.error": "Impossible de copier l'ID de session",
|
"sessionList.copyId.error": "Impossible de copier l'ID de session",
|
||||||
"sessionList.delete.error": "Impossible de supprimer la session",
|
"sessionList.delete.error": "Impossible de supprimer la session",
|
||||||
|
"sessionList.delete.title": "Supprimer la session",
|
||||||
|
"sessionList.delete.confirmMessage": "Supprimer \"{label}\" ? Cette action est irréversible.",
|
||||||
|
"sessionList.delete.confirmLabel": "Supprimer",
|
||||||
|
"sessionList.delete.cancelLabel": "Annuler",
|
||||||
"sessionList.rename.error": "Impossible de renommer la session",
|
"sessionList.rename.error": "Impossible de renommer la session",
|
||||||
|
|
||||||
|
"sessionList.filter.placeholder": "Rechercher des sessions…",
|
||||||
|
"sessionList.filter.ariaLabel": "Rechercher des sessions",
|
||||||
|
"sessionList.selection.selectAllLabel": "Tout sélectionner",
|
||||||
|
"sessionList.selection.selectAllAriaLabel": "Sélectionner toutes les sessions",
|
||||||
|
"sessionList.selection.clearLabel": "Effacer",
|
||||||
|
"sessionList.selection.clearAriaLabel": "Effacer la sélection",
|
||||||
|
"sessionList.selection.checkboxAriaLabel": "Sélectionner la session",
|
||||||
|
"sessionList.bulkDelete.button": "Supprimer {count}",
|
||||||
|
"sessionList.bulkDelete.ariaLabel": "Supprimer {count} sessions sélectionnées",
|
||||||
|
"sessionList.bulkDelete.title": "Supprimer des sessions",
|
||||||
|
"sessionList.bulkDelete.confirmMessage": "Supprimer {count} sessions sélectionnées ? Cette action est irréversible.",
|
||||||
|
"sessionList.bulkDelete.confirmLabel": "Supprimer",
|
||||||
|
"sessionList.bulkDelete.cancelLabel": "Annuler",
|
||||||
|
"sessionList.bulkDelete.error": "Impossible de supprimer {count} sessions",
|
||||||
|
|
||||||
"sessionRenameDialog.title": "Renommer la session",
|
"sessionRenameDialog.title": "Renommer la session",
|
||||||
"sessionRenameDialog.description.withLabel": "Mettre à jour le titre de \"{label}\".",
|
"sessionRenameDialog.description.withLabel": "Mettre à jour le titre de \"{label}\".",
|
||||||
"sessionRenameDialog.description.default": "Définir un nouveau titre pour cette session.",
|
"sessionRenameDialog.description.default": "Définir un nouveau titre pour cette session.",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "セッションへ移動",
|
"messageBlock.tool.goToSession.label": "セッションへ移動",
|
||||||
"messageBlock.tool.goToSession.title": "セッションへ移動",
|
"messageBlock.tool.goToSession.title": "セッションへ移動",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません",
|
"messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません",
|
||||||
|
"messageBlock.tool.deletePart.label": "削除",
|
||||||
|
"messageBlock.tool.deletePart.deleting": "削除中...",
|
||||||
|
"messageBlock.tool.deletePart.title": "このツール出力を削除",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "削除に失敗しました",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "ツール出力の削除に失敗しました",
|
||||||
|
|
||||||
"messageBlock.compaction.ariaLabel": "セッションのコンパクト化",
|
"messageBlock.compaction.ariaLabel": "セッションのコンパクト化",
|
||||||
"messageBlock.compaction.autoLabel": "セッションを自動でコンパクト化しました",
|
"messageBlock.compaction.autoLabel": "セッションを自動でコンパクト化しました",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"messageItem.status.generating": "生成中...",
|
"messageItem.status.generating": "生成中...",
|
||||||
"messageItem.status.sending": "送信中...",
|
"messageItem.status.sending": "送信中...",
|
||||||
"messageItem.status.failedToSend": "メッセージの送信に失敗しました",
|
"messageItem.status.failedToSend": "メッセージの送信に失敗しました",
|
||||||
|
"messagePart.actions.delete": "削除",
|
||||||
|
"messagePart.actions.deleting": "削除中...",
|
||||||
|
"messagePart.actions.deleteTitle": "この項目を削除",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "削除に失敗しました",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "項目の削除に失敗しました",
|
||||||
"messageItem.attachment.defaultName": "添付ファイル",
|
"messageItem.attachment.defaultName": "添付ファイル",
|
||||||
"messageItem.attachment.downloadAriaLabel": "{name} をダウンロード",
|
"messageItem.attachment.downloadAriaLabel": "{name} をダウンロード",
|
||||||
"messageItem.agentMeta.agentLabel": "エージェント: {agent}",
|
"messageItem.agentMeta.agentLabel": "エージェント: {agent}",
|
||||||
|
|||||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
|||||||
"sessionList.copyId.success": "セッション ID をコピーしました",
|
"sessionList.copyId.success": "セッション ID をコピーしました",
|
||||||
"sessionList.copyId.error": "セッション ID をコピーできません",
|
"sessionList.copyId.error": "セッション ID をコピーできません",
|
||||||
"sessionList.delete.error": "セッションを削除できません",
|
"sessionList.delete.error": "セッションを削除できません",
|
||||||
|
"sessionList.delete.title": "セッションを削除",
|
||||||
|
"sessionList.delete.confirmMessage": "\"{label}\" を削除しますか?この操作は元に戻せません。",
|
||||||
|
"sessionList.delete.confirmLabel": "削除",
|
||||||
|
"sessionList.delete.cancelLabel": "キャンセル",
|
||||||
"sessionList.rename.error": "セッション名を変更できません",
|
"sessionList.rename.error": "セッション名を変更できません",
|
||||||
|
|
||||||
|
"sessionList.filter.placeholder": "セッションを検索…",
|
||||||
|
"sessionList.filter.ariaLabel": "セッションを検索",
|
||||||
|
"sessionList.selection.selectAllLabel": "すべて選択",
|
||||||
|
"sessionList.selection.selectAllAriaLabel": "すべてのセッションを選択",
|
||||||
|
"sessionList.selection.clearLabel": "クリア",
|
||||||
|
"sessionList.selection.clearAriaLabel": "選択をクリア",
|
||||||
|
"sessionList.selection.checkboxAriaLabel": "セッションを選択",
|
||||||
|
"sessionList.bulkDelete.button": "{count} 件を削除",
|
||||||
|
"sessionList.bulkDelete.ariaLabel": "選択した {count} 件のセッションを削除",
|
||||||
|
"sessionList.bulkDelete.title": "セッションを削除",
|
||||||
|
"sessionList.bulkDelete.confirmMessage": "選択した {count} 件のセッションを削除しますか?この操作は元に戻せません。",
|
||||||
|
"sessionList.bulkDelete.confirmLabel": "削除",
|
||||||
|
"sessionList.bulkDelete.cancelLabel": "キャンセル",
|
||||||
|
"sessionList.bulkDelete.error": "{count} 件のセッションを削除できません",
|
||||||
|
|
||||||
"sessionRenameDialog.title": "セッション名を変更",
|
"sessionRenameDialog.title": "セッション名を変更",
|
||||||
"sessionRenameDialog.description.withLabel": "\"{label}\" のタイトルを更新します。",
|
"sessionRenameDialog.description.withLabel": "\"{label}\" のタイトルを更新します。",
|
||||||
"sessionRenameDialog.description.default": "このセッションの新しいタイトルを設定します。",
|
"sessionRenameDialog.description.default": "このセッションの新しいタイトルを設定します。",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Перейти к сессии",
|
"messageBlock.tool.goToSession.label": "Перейти к сессии",
|
||||||
"messageBlock.tool.goToSession.title": "Перейти к сессии",
|
"messageBlock.tool.goToSession.title": "Перейти к сессии",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна",
|
"messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна",
|
||||||
|
"messageBlock.tool.deletePart.label": "Удалить",
|
||||||
|
"messageBlock.tool.deletePart.deleting": "Удаление...",
|
||||||
|
"messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "Ошибка удаления",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "Не удалось удалить вывод инструмента",
|
||||||
|
|
||||||
"messageBlock.compaction.ariaLabel": "Компактация сессии",
|
"messageBlock.compaction.ariaLabel": "Компактация сессии",
|
||||||
"messageBlock.compaction.autoLabel": "Сессия автоматически компактирована",
|
"messageBlock.compaction.autoLabel": "Сессия автоматически компактирована",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"messageItem.status.generating": "Генерация…",
|
"messageItem.status.generating": "Генерация…",
|
||||||
"messageItem.status.sending": "Отправка…",
|
"messageItem.status.sending": "Отправка…",
|
||||||
"messageItem.status.failedToSend": "Не удалось отправить сообщение",
|
"messageItem.status.failedToSend": "Не удалось отправить сообщение",
|
||||||
|
"messagePart.actions.delete": "Удалить",
|
||||||
|
"messagePart.actions.deleting": "Удаление...",
|
||||||
|
"messagePart.actions.deleteTitle": "Удалить этот элемент",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "Ошибка удаления",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "Не удалось удалить элемент",
|
||||||
"messageItem.attachment.defaultName": "вложение",
|
"messageItem.attachment.defaultName": "вложение",
|
||||||
"messageItem.attachment.downloadAriaLabel": "Скачать {name}",
|
"messageItem.attachment.downloadAriaLabel": "Скачать {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "Агент: {agent}",
|
"messageItem.agentMeta.agentLabel": "Агент: {agent}",
|
||||||
|
|||||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
|||||||
"sessionList.copyId.success": "ID сессии скопирован",
|
"sessionList.copyId.success": "ID сессии скопирован",
|
||||||
"sessionList.copyId.error": "Не удалось скопировать ID сессии",
|
"sessionList.copyId.error": "Не удалось скопировать ID сессии",
|
||||||
"sessionList.delete.error": "Не удалось удалить сессию",
|
"sessionList.delete.error": "Не удалось удалить сессию",
|
||||||
|
"sessionList.delete.title": "Удалить сессию",
|
||||||
|
"sessionList.delete.confirmMessage": "Удалить \"{label}\"? Это действие нельзя отменить.",
|
||||||
|
"sessionList.delete.confirmLabel": "Удалить",
|
||||||
|
"sessionList.delete.cancelLabel": "Отмена",
|
||||||
"sessionList.rename.error": "Не удалось переименовать сессию",
|
"sessionList.rename.error": "Не удалось переименовать сессию",
|
||||||
|
|
||||||
|
"sessionList.filter.placeholder": "Поиск сессий…",
|
||||||
|
"sessionList.filter.ariaLabel": "Поиск сессий",
|
||||||
|
"sessionList.selection.selectAllLabel": "Выбрать все",
|
||||||
|
"sessionList.selection.selectAllAriaLabel": "Выбрать все сессии",
|
||||||
|
"sessionList.selection.clearLabel": "Очистить",
|
||||||
|
"sessionList.selection.clearAriaLabel": "Очистить выбор",
|
||||||
|
"sessionList.selection.checkboxAriaLabel": "Выбрать сессию",
|
||||||
|
"sessionList.bulkDelete.button": "Удалить {count}",
|
||||||
|
"sessionList.bulkDelete.ariaLabel": "Удалить {count} выбранных сессий",
|
||||||
|
"sessionList.bulkDelete.title": "Удалить сессии",
|
||||||
|
"sessionList.bulkDelete.confirmMessage": "Удалить {count} выбранных сессий? Это действие нельзя отменить.",
|
||||||
|
"sessionList.bulkDelete.confirmLabel": "Удалить",
|
||||||
|
"sessionList.bulkDelete.cancelLabel": "Отмена",
|
||||||
|
"sessionList.bulkDelete.error": "Не удалось удалить {count} сессий",
|
||||||
|
|
||||||
"sessionRenameDialog.title": "Переименовать сессию",
|
"sessionRenameDialog.title": "Переименовать сессию",
|
||||||
"sessionRenameDialog.description.withLabel": "Обновите название для \"{label}\".",
|
"sessionRenameDialog.description.withLabel": "Обновите название для \"{label}\".",
|
||||||
"sessionRenameDialog.description.default": "Установите новое название для этой сессии.",
|
"sessionRenameDialog.description.default": "Установите новое название для этой сессии.",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "前往会话",
|
"messageBlock.tool.goToSession.label": "前往会话",
|
||||||
"messageBlock.tool.goToSession.title": "前往会话",
|
"messageBlock.tool.goToSession.title": "前往会话",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用",
|
"messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用",
|
||||||
|
"messageBlock.tool.deletePart.label": "删除",
|
||||||
|
"messageBlock.tool.deletePart.deleting": "正在删除...",
|
||||||
|
"messageBlock.tool.deletePart.title": "删除此工具输出",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "删除失败",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "删除工具输出失败",
|
||||||
|
|
||||||
"messageBlock.compaction.ariaLabel": "会话压缩",
|
"messageBlock.compaction.ariaLabel": "会话压缩",
|
||||||
"messageBlock.compaction.autoLabel": "会话已自动压缩",
|
"messageBlock.compaction.autoLabel": "会话已自动压缩",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"messageItem.status.generating": "正在生成...",
|
"messageItem.status.generating": "正在生成...",
|
||||||
"messageItem.status.sending": "正在发送...",
|
"messageItem.status.sending": "正在发送...",
|
||||||
"messageItem.status.failedToSend": "消息发送失败",
|
"messageItem.status.failedToSend": "消息发送失败",
|
||||||
|
"messagePart.actions.delete": "删除",
|
||||||
|
"messagePart.actions.deleting": "正在删除...",
|
||||||
|
"messagePart.actions.deleteTitle": "删除此项",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "删除失败",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "删除失败",
|
||||||
"messageItem.attachment.defaultName": "附件",
|
"messageItem.attachment.defaultName": "附件",
|
||||||
"messageItem.attachment.downloadAriaLabel": "下载 {name}",
|
"messageItem.attachment.downloadAriaLabel": "下载 {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "智能体:{agent}",
|
"messageItem.agentMeta.agentLabel": "智能体:{agent}",
|
||||||
|
|||||||
@@ -30,8 +30,27 @@ export const sessionMessages = {
|
|||||||
"sessionList.copyId.success": "已复制会话 ID",
|
"sessionList.copyId.success": "已复制会话 ID",
|
||||||
"sessionList.copyId.error": "无法复制会话 ID",
|
"sessionList.copyId.error": "无法复制会话 ID",
|
||||||
"sessionList.delete.error": "无法删除会话",
|
"sessionList.delete.error": "无法删除会话",
|
||||||
|
"sessionList.delete.title": "删除会话",
|
||||||
|
"sessionList.delete.confirmMessage": "删除“{label}”?此操作无法撤销。",
|
||||||
|
"sessionList.delete.confirmLabel": "删除",
|
||||||
|
"sessionList.delete.cancelLabel": "取消",
|
||||||
"sessionList.rename.error": "无法重命名会话",
|
"sessionList.rename.error": "无法重命名会话",
|
||||||
|
|
||||||
|
"sessionList.filter.placeholder": "搜索会话…",
|
||||||
|
"sessionList.filter.ariaLabel": "搜索会话",
|
||||||
|
"sessionList.selection.selectAllLabel": "全选",
|
||||||
|
"sessionList.selection.selectAllAriaLabel": "选择所有会话",
|
||||||
|
"sessionList.selection.clearLabel": "清除",
|
||||||
|
"sessionList.selection.clearAriaLabel": "清除选择",
|
||||||
|
"sessionList.selection.checkboxAriaLabel": "选择会话",
|
||||||
|
"sessionList.bulkDelete.button": "删除 {count}",
|
||||||
|
"sessionList.bulkDelete.ariaLabel": "删除已选择的 {count} 个会话",
|
||||||
|
"sessionList.bulkDelete.title": "删除会话",
|
||||||
|
"sessionList.bulkDelete.confirmMessage": "删除已选择的 {count} 个会话?此操作无法撤销。",
|
||||||
|
"sessionList.bulkDelete.confirmLabel": "删除",
|
||||||
|
"sessionList.bulkDelete.cancelLabel": "取消",
|
||||||
|
"sessionList.bulkDelete.error": "无法删除 {count} 个会话",
|
||||||
|
|
||||||
"sessionRenameDialog.title": "重命名会话",
|
"sessionRenameDialog.title": "重命名会话",
|
||||||
"sessionRenameDialog.description.withLabel": "更新“{label}”的标题。",
|
"sessionRenameDialog.description.withLabel": "更新“{label}”的标题。",
|
||||||
"sessionRenameDialog.description.default": "为此会话设置新标题。",
|
"sessionRenameDialog.description.default": "为此会话设置新标题。",
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ async function getOrCreateHighlighter() {
|
|||||||
|
|
||||||
// Create highlighter with no preloaded languages
|
// Create highlighter with no preloaded languages
|
||||||
highlighterPromise = createHighlighter({
|
highlighterPromise = createHighlighter({
|
||||||
themes: ["github-light", "github-dark"],
|
themes: ["github-light", "github-light-high-contrast", "github-dark"],
|
||||||
langs: [],
|
langs: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -242,9 +242,9 @@ async function runLanguageLoadQueue() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupRenderer(isDark: boolean) {
|
function setupRenderer(isDark: boolean) {
|
||||||
if (!highlighter || rendererSetup) return
|
|
||||||
|
|
||||||
currentTheme = isDark ? "dark" : "light"
|
currentTheme = isDark ? "dark" : "light"
|
||||||
|
if (!highlighter) return
|
||||||
|
if (rendererSetup) return
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
@@ -298,7 +298,7 @@ function setupRenderer(isDark: boolean) {
|
|||||||
try {
|
try {
|
||||||
const html = highlighter!.codeToHtml(decodedCode, {
|
const html = highlighter!.codeToHtml(decodedCode, {
|
||||||
lang: langKey,
|
lang: langKey,
|
||||||
theme: currentTheme === "dark" ? "github-dark" : "github-light",
|
theme: currentTheme === "dark" ? "github-dark" : "github-light-high-contrast",
|
||||||
})
|
})
|
||||||
return `<div class="markdown-code-block" data-language="${escapedLang}" data-code="${encodedCode}">${header}${html}</div>`
|
return `<div class="markdown-code-block" data-language="${escapedLang}" data-code="${encodedCode}">${header}${html}</div>`
|
||||||
} catch {
|
} catch {
|
||||||
@@ -329,6 +329,10 @@ export async function initMarkdown(isDark: boolean) {
|
|||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setMarkdownTheme(isDark: boolean) {
|
||||||
|
currentTheme = isDark ? "dark" : "light"
|
||||||
|
}
|
||||||
|
|
||||||
export function isMarkdownReady(): boolean {
|
export function isMarkdownReady(): boolean {
|
||||||
return isInitialized && highlighter !== null
|
return isInitialized && highlighter !== null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import toast from "solid-toast"
|
import toast from "solid-toast"
|
||||||
|
import { isTauriHost } from "./runtime-env"
|
||||||
|
|
||||||
export type ToastVariant = "info" | "success" | "warning" | "error"
|
export type ToastVariant = "info" | "success" | "warning" | "error"
|
||||||
|
|
||||||
@@ -21,6 +22,31 @@ export type ToastPayload = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openExternalUrl(url: string): Promise<void> {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isTauriHost()) {
|
||||||
|
const { openUrl } = await import("@tauri-apps/plugin-opener")
|
||||||
|
await openUrl(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Fall through to browser handling.
|
||||||
|
// Note: on Linux, system opener failures can throw here.
|
||||||
|
console.warn("[notifications] unable to open via system opener", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer")
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[notifications] unable to open external url", error)
|
||||||
|
toast.error("Unable to open link")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const variantAccent: Record<
|
const variantAccent: Record<
|
||||||
ToastVariant,
|
ToastVariant,
|
||||||
{
|
{
|
||||||
@@ -80,14 +106,13 @@ export function showToastNotification(payload: ToastPayload): ToastHandle {
|
|||||||
{payload.title && <p class={`font-semibold ${accent.headline}`}>{payload.title}</p>}
|
{payload.title && <p class={`font-semibold ${accent.headline}`}>{payload.title}</p>}
|
||||||
<p class={`${accent.body} ${payload.title ? "mt-1" : ""}`}>{payload.message}</p>
|
<p class={`${accent.body} ${payload.title ? "mt-1" : ""}`}>{payload.message}</p>
|
||||||
{payload.action && (
|
{payload.action && (
|
||||||
<a
|
<button
|
||||||
|
type="button"
|
||||||
class="mt-3 inline-flex items-center text-xs font-semibold uppercase tracking-wide text-sky-300 hover:text-sky-200"
|
class="mt-3 inline-flex items-center text-xs font-semibold uppercase tracking-wide text-sky-300 hover:text-sky-200"
|
||||||
href={payload.action.href}
|
onClick={() => void openExternalUrl(payload.action!.href)}
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
>
|
>
|
||||||
{payload.action.label}
|
{payload.action.label}
|
||||||
</a>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,22 +3,24 @@ import { createTheme, ThemeProvider as MuiThemeProvider } from "@suid/material/s
|
|||||||
import CssBaseline from "@suid/material/CssBaseline"
|
import CssBaseline from "@suid/material/CssBaseline"
|
||||||
import { useConfig } from "../stores/preferences"
|
import { useConfig } from "../stores/preferences"
|
||||||
|
|
||||||
|
export type ThemeMode = "system" | "light" | "dark"
|
||||||
|
|
||||||
interface ThemeContextValue {
|
interface ThemeContextValue {
|
||||||
isDark: () => boolean
|
isDark: () => boolean
|
||||||
toggleTheme: () => void
|
themeMode: () => ThemeMode
|
||||||
setTheme: (dark: boolean) => void
|
setThemeMode: (mode: ThemeMode) => void
|
||||||
|
cycleThemeMode: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextValue>()
|
const ThemeContext = createContext<ThemeContextValue>()
|
||||||
|
|
||||||
function applyTheme(dark: boolean) {
|
function applyThemeMode(mode: ThemeMode) {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
if (dark) {
|
if (mode === "system") {
|
||||||
document.documentElement.setAttribute("data-theme", "dark")
|
document.documentElement.removeAttribute("data-theme")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
document.documentElement.setAttribute("data-theme", mode)
|
||||||
document.documentElement.removeAttribute("data-theme")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResolvedPaletteColors {
|
interface ResolvedPaletteColors {
|
||||||
@@ -78,16 +80,31 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
|||||||
const mediaQuery = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null
|
const mediaQuery = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null
|
||||||
const { themePreference, setThemePreference } = useConfig()
|
const { themePreference, setThemePreference } = useConfig()
|
||||||
const [isDark, setIsDarkSignal] = createSignal(true)
|
const [isDark, setIsDarkSignal] = createSignal(true)
|
||||||
|
const [themeRevision, setThemeRevision] = createSignal(0)
|
||||||
|
|
||||||
|
const themeMode = () => themePreference() as ThemeMode
|
||||||
|
|
||||||
const resolveDarkTheme = () => {
|
const resolveDarkTheme = () => {
|
||||||
themePreference()
|
const mode = themeMode()
|
||||||
return true
|
if (mode === "dark") return true
|
||||||
|
if (mode === "light") return false
|
||||||
|
return mediaQuery?.matches ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyResolvedTheme = () => {
|
const applyResolvedTheme = () => {
|
||||||
|
const mode = themeMode()
|
||||||
const dark = resolveDarkTheme()
|
const dark = resolveDarkTheme()
|
||||||
|
if (mode === "system") {
|
||||||
|
applyThemeMode("system")
|
||||||
|
} else {
|
||||||
|
applyThemeMode(mode)
|
||||||
|
}
|
||||||
setIsDarkSignal(dark)
|
setIsDarkSignal(dark)
|
||||||
applyTheme(dark)
|
if (typeof window !== "undefined") {
|
||||||
|
requestAnimationFrame(() => setThemeRevision((v) => v + 1))
|
||||||
|
} else {
|
||||||
|
setThemeRevision((v) => v + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -107,15 +124,18 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const setTheme = (_dark: boolean) => {
|
const setThemeMode = (mode: ThemeMode) => {
|
||||||
setThemePreference("dark")
|
setThemePreference(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const cycleThemeMode = () => {
|
||||||
setTheme(true)
|
const current = themeMode()
|
||||||
|
const next: ThemeMode = current === "system" ? "light" : current === "light" ? "dark" : "system"
|
||||||
|
setThemeMode(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
const muiTheme = createMemo(() => {
|
const muiTheme = createMemo(() => {
|
||||||
|
themeRevision()
|
||||||
const paletteColors = resolvePaletteColors(isDark())
|
const paletteColors = resolvePaletteColors(isDark())
|
||||||
return createTheme({
|
return createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@@ -144,21 +164,32 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
MuiIconButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
color: "inherit",
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
opacity: 0.55,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiDrawer: {
|
MuiDrawer: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
paper: {
|
paper: {
|
||||||
backgroundColor: paletteColors.backgroundPaper,
|
backgroundColor: "var(--surface-secondary)",
|
||||||
color: paletteColors.textPrimary,
|
color: "var(--text-primary)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MuiAppBar: {
|
MuiAppBar: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: paletteColors.backgroundPaper,
|
backgroundColor: "var(--surface-secondary)",
|
||||||
color: paletteColors.textPrimary,
|
color: "var(--text-primary)",
|
||||||
boxShadow: "none",
|
boxShadow: "none",
|
||||||
borderBottom: `1px solid ${paletteColors.divider}`,
|
borderBottom: "1px solid var(--border-base)",
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -175,7 +206,7 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ isDark, toggleTheme, setTheme }}>
|
<ThemeContext.Provider value={{ isDark, themeMode, setThemeMode, cycleThemeMode }}>
|
||||||
<MuiThemeProvider theme={muiTheme()}>
|
<MuiThemeProvider theme={muiTheme()}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ConfigProvider } from "./stores/preferences"
|
|||||||
import { InstanceConfigProvider } from "./stores/instance-config"
|
import { InstanceConfigProvider } from "./stores/instance-config"
|
||||||
import { runtimeEnv } from "./lib/runtime-env"
|
import { runtimeEnv } from "./lib/runtime-env"
|
||||||
import { I18nProvider } from "./lib/i18n"
|
import { I18nProvider } from "./lib/i18n"
|
||||||
|
import { storage } from "./lib/storage"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||||
|
|
||||||
@@ -14,11 +15,34 @@ if (!root) {
|
|||||||
throw new Error("Root element not found")
|
throw new Error("Root element not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mount = root
|
||||||
|
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.documentElement.dataset.runtimeHost = runtimeEnv.host
|
document.documentElement.dataset.runtimeHost = runtimeEnv.host
|
||||||
document.documentElement.dataset.runtimePlatform = runtimeEnv.platform
|
document.documentElement.dataset.runtimePlatform = runtimeEnv.platform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
// renderer/index.html currently seeds a dark theme to avoid a white flash.
|
||||||
|
// Reset to CSS defaults immediately so the first render matches system
|
||||||
|
// (and then refine once persisted config loads).
|
||||||
|
document.documentElement.removeAttribute("data-theme")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = await storage.loadConfig()
|
||||||
|
const theme = config?.theme ?? "system"
|
||||||
|
|
||||||
|
if (theme === "system") {
|
||||||
|
document.documentElement.removeAttribute("data-theme")
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute("data-theme", theme)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If config fails to load, fall back to CSS defaults.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
@@ -31,5 +55,8 @@ render(
|
|||||||
</InstanceConfigProvider>
|
</InstanceConfigProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
),
|
),
|
||||||
root,
|
mount,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
void bootstrap()
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: light dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: var(--surface-base, #0f141f);
|
background-color: var(--surface-base, #ffffff);
|
||||||
color: var(--text-primary, #cfd4dc);
|
color: var(--text-primary, #1a1a1a);
|
||||||
font-family: var(--font-family-sans, "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
|
font-family: var(--font-family-sans, "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -34,7 +34,7 @@ button {
|
|||||||
.loading-logo {
|
.loading-logo {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
height: auto;
|
height: auto;
|
||||||
filter: drop-shadow(0 20px 60px rgba(0, 0, 0, 0.45));
|
filter: drop-shadow(0 20px 60px rgba(0, 0, 0, 0.18));
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-heading {
|
.loading-heading {
|
||||||
@@ -47,13 +47,13 @@ button {
|
|||||||
font-size: 2.8rem;
|
font-size: 2.8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-primary, #f4f6fb);
|
color: var(--text-primary, #1a1a1a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-status {
|
.loading-status {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--text-muted, #aeb3c4);
|
color: var(--text-muted, #666666);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-card {
|
.loading-card {
|
||||||
@@ -62,9 +62,9 @@ button {
|
|||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: rgba(13, 16, 24, 0.85);
|
background: var(--surface-secondary, #f5f5f5);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid var(--border-base, #e0e0e0);
|
||||||
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.55);
|
box-shadow: var(--panel-shadow-strong, 0 25px 60px rgba(0, 0, 0, 0.16));
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-row {
|
.loading-row {
|
||||||
@@ -79,28 +79,74 @@ button {
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid rgba(255, 255, 255, 0.18);
|
border: 2px solid color-mix(in srgb, var(--text-primary, #1a1a1a) 14%, transparent);
|
||||||
border-top-color: #6ce3ff;
|
border-top-color: var(--accent-primary, #0066ff);
|
||||||
animation: spin 0.9s linear infinite;
|
animation: spin 0.9s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phrase-controls {
|
.phrase-controls {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: var(--text-muted, #8f96a9);
|
color: var(--text-muted, #666666);
|
||||||
}
|
}
|
||||||
|
|
||||||
.phrase-controls button {
|
.phrase-controls button {
|
||||||
color: #8fb5ff;
|
color: var(--accent-primary, #0066ff);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-error {
|
.loading-error {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
color: #ff9ea9;
|
color: var(--status-error, #dc2626);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-theme="light"]) body {
|
||||||
|
background-color: var(--surface-base, #0f141f);
|
||||||
|
color: var(--text-primary, #cfd4dc);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-theme="light"]) .loading-logo {
|
||||||
|
filter: drop-shadow(0 20px 60px rgba(0, 0, 0, 0.45));
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-theme="light"]) .loading-title {
|
||||||
|
color: var(--text-primary, #f4f6fb);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-theme="light"]) .loading-status {
|
||||||
|
color: var(--text-muted, #aeb3c4);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-theme="light"]) .loading-card {
|
||||||
|
background: rgba(13, 16, 24, 0.85);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-theme="light"]) .spinner {
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.18);
|
||||||
|
border-top-color: var(--accent-primary, #6ce3ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-theme="light"]) .phrase-controls {
|
||||||
|
color: var(--text-muted, #8f96a9);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-theme="light"]) .loading-error {
|
||||||
|
color: var(--status-error, #ff9ea9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ const [isConfigLoaded, setIsConfigLoaded] = createSignal(false)
|
|||||||
const preferences = createMemo<Preferences>(() => internalConfig().preferences)
|
const preferences = createMemo<Preferences>(() => internalConfig().preferences)
|
||||||
const recentFolders = createMemo<RecentFolder[]>(() => internalConfig().recentFolders ?? [])
|
const recentFolders = createMemo<RecentFolder[]>(() => internalConfig().recentFolders ?? [])
|
||||||
const opencodeBinaries = createMemo<OpenCodeBinary[]>(() => internalConfig().opencodeBinaries ?? [])
|
const opencodeBinaries = createMemo<OpenCodeBinary[]>(() => internalConfig().opencodeBinaries ?? [])
|
||||||
const themePreference = createMemo<ThemePreference>(() => internalConfig().theme ?? "dark")
|
const themePreference = createMemo<ThemePreference>(() => internalConfig().theme ?? "system")
|
||||||
let loadPromise: Promise<void> | null = null
|
let loadPromise: Promise<void> | null = null
|
||||||
|
|
||||||
function normalizeConfig(config?: ConfigData | null): ConfigData {
|
function normalizeConfig(config?: ConfigData | null): ConfigData {
|
||||||
@@ -202,7 +202,7 @@ function normalizeConfig(config?: ConfigData | null): ConfigData {
|
|||||||
preferences: normalizePreferences(config?.preferences),
|
preferences: normalizePreferences(config?.preferences),
|
||||||
recentFolders: (config?.recentFolders ?? []).map((folder) => ({ ...folder })),
|
recentFolders: (config?.recentFolders ?? []).map((folder) => ({ ...folder })),
|
||||||
opencodeBinaries: (config?.opencodeBinaries ?? []).map((binary) => ({ ...binary })),
|
opencodeBinaries: (config?.opencodeBinaries ?? []).map((binary) => ({ ...binary })),
|
||||||
theme: config?.theme ?? "dark",
|
theme: config?.theme ?? "system",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { providers, sessions, withSession } from "./session-state"
|
|||||||
import { getDefaultModel, isModelValid } from "./session-models"
|
import { getDefaultModel, isModelValid } from "./session-models"
|
||||||
import { updateSessionInfo } from "./message-v2/session-info"
|
import { updateSessionInfo } from "./message-v2/session-info"
|
||||||
import { messageStoreBus } from "./message-v2/bus"
|
import { messageStoreBus } from "./message-v2/bus"
|
||||||
|
import { removeMessagePartV2 } from "./message-v2/bridge"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { requestData } from "../lib/opencode-api"
|
import { requestData } from "../lib/opencode-api"
|
||||||
|
|
||||||
@@ -395,8 +396,30 @@ async function renameSession(instanceId: string, sessionId: string, nextTitle: s
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteMessagePart(instanceId: string, sessionId: string, messageId: string, partId: string): Promise<void> {
|
||||||
|
if (!instanceId || !sessionId || !messageId || !partId) return
|
||||||
|
const instance = instances().get(instanceId)
|
||||||
|
if (!instance || !instance.client) {
|
||||||
|
throw new Error("Instance not ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
await requestData(
|
||||||
|
instance.client.part.delete({
|
||||||
|
sessionID: sessionId,
|
||||||
|
messageID: messageId,
|
||||||
|
partID: partId,
|
||||||
|
}),
|
||||||
|
"part.delete",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Optimistic removal; SSE will also broadcast a part-removed event.
|
||||||
|
removeMessagePartV2(instanceId, messageId, partId)
|
||||||
|
updateSessionInfo(instanceId, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
abortSession,
|
abortSession,
|
||||||
|
deleteMessagePart,
|
||||||
executeCustomCommand,
|
executeCustomCommand,
|
||||||
renameSession,
|
renameSession,
|
||||||
runShellCommand,
|
runShellCommand,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
.permission-center-modal-backdrop {
|
.permission-center-modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: color-mix(in srgb, var(--text-inverted) 55%, transparent);
|
background: var(--overlay-scrim);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--border-base);
|
||||||
background: var(--surface-base);
|
background: var(--surface-base);
|
||||||
box-shadow: var(--panel-shadow, 0 12px 32px rgba(0, 0, 0, 0.25));
|
box-shadow: var(--panel-shadow-strong);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,8 @@
|
|||||||
|
|
||||||
.message-reasoning {
|
.message-reasoning {
|
||||||
@apply my-2 border rounded;
|
@apply my-2 border rounded;
|
||||||
border-color: var(--border-base);
|
--reasoning-border-color: var(--border-strong, var(--border-base));
|
||||||
|
border-color: var(--reasoning-border-color);
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
@@ -286,6 +287,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-reasoning-card {
|
.message-reasoning-card {
|
||||||
|
--reasoning-border-color: var(--border-strong, var(--border-base));
|
||||||
background-color: var(--message-assistant-bg);
|
background-color: var(--message-assistant-bg);
|
||||||
border-left: 4px solid var(--message-assistant-border);
|
border-left: 4px solid var(--message-assistant-border);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@@ -339,7 +341,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
padding: 0 0.75rem;
|
padding: 0 0.75rem;
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--reasoning-border-color, var(--border-base));
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -381,6 +383,7 @@
|
|||||||
@apply flex flex-col;
|
@apply flex flex-col;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--reasoning-border-color, var(--border-base));
|
||||||
max-height: 30rem;
|
max-height: 30rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
@@ -397,4 +400,3 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,7 +244,7 @@
|
|||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
border: 1px solid var(--list-item-highlight-border);
|
border: 1px solid var(--list-item-highlight-border);
|
||||||
background-color: var(--list-item-highlight-bg-solid);
|
background-color: var(--list-item-highlight-bg-solid);
|
||||||
box-shadow: var(--panel-shadow, 0 4px 16px rgba(0, 0, 0, 0.2));
|
box-shadow: var(--panel-shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
box-shadow: var(--panel-shadow, 0 6px 24px rgba(0, 0, 0, 0.2));
|
box-shadow: var(--panel-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-timeline::-webkit-scrollbar {
|
.message-timeline::-webkit-scrollbar {
|
||||||
@@ -103,11 +103,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-timeline-segment-active {
|
.message-timeline-segment-active {
|
||||||
border-color: transparent;
|
border-color: color-mix(in oklab, var(--timeline-segment-active-bg) 92%, var(--timeline-segment-active-text));
|
||||||
background-color: #0f5b44;
|
background-color: var(--timeline-segment-active-bg);
|
||||||
color: #fff;
|
color: var(--timeline-segment-active-text);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
|
box-shadow: var(--timeline-segment-active-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-timeline-segment:hover,
|
.message-timeline-segment:hover,
|
||||||
@@ -121,10 +121,10 @@
|
|||||||
.message-timeline-segment-active,
|
.message-timeline-segment-active,
|
||||||
.message-timeline-segment-active:hover,
|
.message-timeline-segment-active:hover,
|
||||||
.message-timeline-segment-active:focus-visible {
|
.message-timeline-segment-active:focus-visible {
|
||||||
background-color: #0f5b44;
|
background-color: var(--timeline-segment-active-bg);
|
||||||
color: #fff;
|
color: var(--timeline-segment-active-text);
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
|
box-shadow: var(--timeline-segment-active-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-timeline-segment:focus-visible {
|
.message-timeline-segment:focus-visible {
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
border-color: var(--session-status-permission-fg) !important;
|
border-color: var(--session-status-permission-fg) !important;
|
||||||
color: var(--session-status-permission-fg) !important;
|
color: var(--session-status-permission-fg) !important;
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
|
box-shadow: var(--timeline-segment-active-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-timeline-compaction-auto {
|
.message-timeline-compaction-auto {
|
||||||
@@ -181,11 +181,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-timeline-segment-active {
|
.message-timeline-segment-active {
|
||||||
background-color: #0f5b44 !important;
|
background-color: var(--timeline-segment-active-bg) !important;
|
||||||
border-color: transparent !important;
|
border-color: color-mix(in oklab, var(--timeline-segment-active-bg) 92%, var(--timeline-segment-active-text)) !important;
|
||||||
color: #fff !important;
|
color: var(--timeline-segment-active-text) !important;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
|
box-shadow: var(--timeline-segment-active-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-timeline-label {
|
.message-timeline-label {
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--border-base);
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
box-shadow: var(--panel-shadow, 0 12px 32px rgba(0, 0, 0, 0.25));
|
box-shadow: var(--panel-shadow-strong);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
@apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors;
|
@apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
color: inherit;
|
color: var(--text-primary);
|
||||||
|
caret-color: var(--text-primary);
|
||||||
border-color: var(--border-base);
|
border-color: var(--border-base);
|
||||||
line-height: var(--line-height-normal);
|
line-height: var(--line-height-normal);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -97,7 +98,7 @@
|
|||||||
.prompt-history-button {
|
.prompt-history-button {
|
||||||
@apply w-7 h-7 flex items-center justify-center rounded-md;
|
@apply w-7 h-7 flex items-center justify-center rounded-md;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
background-color: rgba(15, 23, 42, 0.04);
|
background-color: var(--control-ghost-bg);
|
||||||
transition: background-color 0.15s ease, color 0.15s ease;
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -143,7 +144,7 @@
|
|||||||
|
|
||||||
.prompt-input.shell-mode {
|
.prompt-input.shell-mode {
|
||||||
border-color: var(--status-success);
|
border-color: var(--status-success);
|
||||||
box-shadow: inset 0 0 0 1px rgba(76, 175, 80, 0.4);
|
box-shadow: inset 0 0 0 1px var(--status-success-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input:focus {
|
.prompt-input:focus {
|
||||||
@@ -152,7 +153,7 @@
|
|||||||
|
|
||||||
.prompt-input.shell-mode:focus {
|
.prompt-input.shell-mode:focus {
|
||||||
border-color: var(--status-success);
|
border-color: var(--status-success);
|
||||||
box-shadow: inset 0 0 0 1px rgba(76, 175, 80, 0.4);
|
box-shadow: inset 0 0 0 1px var(--status-success-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input:disabled {
|
.prompt-input:disabled {
|
||||||
@@ -165,17 +166,17 @@
|
|||||||
|
|
||||||
.stop-button {
|
.stop-button {
|
||||||
@apply w-10 h-10 rounded-md border-none cursor-pointer flex items-center justify-center transition-all flex-shrink-0;
|
@apply w-10 h-10 rounded-md border-none cursor-pointer flex items-center justify-center transition-all flex-shrink-0;
|
||||||
background-color: rgba(239, 68, 68, 0.85);
|
background-color: var(--button-danger-bg);
|
||||||
color: var(--text-inverted);
|
color: var(--button-danger-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop-button:hover:not(:disabled) {
|
.stop-button:hover:not(:disabled) {
|
||||||
background-color: rgba(239, 68, 68, 0.9);
|
background-color: var(--button-danger-hover-bg);
|
||||||
@apply opacity-95 scale-105;
|
@apply opacity-95 scale-105;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop-button:active:not(:disabled) {
|
.stop-button:active:not(:disabled) {
|
||||||
background-color: rgba(239, 68, 68, 1);
|
background-color: var(--button-danger-active-bg);
|
||||||
@apply scale-95;
|
@apply scale-95;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@
|
|||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--border-base);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.25);
|
box-shadow: var(--popover-shadow);
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
.tool-call-header-label {
|
.tool-call-header-label {
|
||||||
@apply flex items-center justify-between gap-2 font-semibold text-sm;
|
@apply flex items-center justify-between gap-2 font-semibold text-sm;
|
||||||
color: var(--message-tool-border);
|
color: var(--text-primary);
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
|
|
||||||
.tool-call-header-button {
|
.tool-call-header-button {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--tool-call-border-color, var(--border-base));
|
||||||
color: var(--message-tool-border);
|
color: var(--text-secondary);
|
||||||
padding: 0.15rem 0.75rem;
|
padding: 0.15rem 0.75rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--tool-call-border-color, var(--border-base));
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -66,7 +66,9 @@
|
|||||||
|
|
||||||
.tool-call {
|
.tool-call {
|
||||||
@apply border overflow-hidden;
|
@apply border overflow-hidden;
|
||||||
border-color: var(--border-base);
|
/* Tokenized so dark mode doesn't get overly bright borders. */
|
||||||
|
--tool-call-border-color: var(--border-strong, var(--border-base));
|
||||||
|
border-color: var(--tool-call-border-color);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
--tool-call-line-unit: 1.4em;
|
--tool-call-line-unit: 1.4em;
|
||||||
--tool-call-lines-compact: 15;
|
--tool-call-lines-compact: 15;
|
||||||
@@ -86,13 +88,14 @@
|
|||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-header::before {
|
.tool-call-header::before {
|
||||||
content: "▶";
|
content: "▶";
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-right: 0.35rem;
|
margin-right: 0.35rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-header[aria-expanded="true"]::before {
|
.tool-call-header[aria-expanded="true"]::before {
|
||||||
@@ -115,6 +118,7 @@
|
|||||||
|
|
||||||
.tool-call-summary {
|
.tool-call-summary {
|
||||||
@apply flex-1 text-left inline-flex items-center gap-2;
|
@apply flex-1 text-left inline-flex items-center gap-2;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-summary::before {
|
.tool-call-summary::before {
|
||||||
@@ -130,6 +134,8 @@
|
|||||||
margin-right: 0.35rem;
|
margin-right: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ToolState uses status="completed"; keep "success" as a legacy alias. */
|
||||||
|
.tool-call-status-completed,
|
||||||
.tool-call-status-success {
|
.tool-call-status-success {
|
||||||
border-left: 3px solid var(--status-success);
|
border-left: 3px solid var(--status-success);
|
||||||
}
|
}
|
||||||
@@ -157,12 +163,12 @@
|
|||||||
.tool-call-preview {
|
.tool-call-preview {
|
||||||
@apply p-2 flex flex-col gap-1.5;
|
@apply p-2 flex flex-col gap-1.5;
|
||||||
background-color: var(--surface-code);
|
background-color: var(--surface-code);
|
||||||
border-top: 1px solid var(--border-base);
|
border-top: 1px solid var(--tool-call-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-preview-label {
|
.tool-call-preview-label {
|
||||||
@apply text-xs font-semibold uppercase tracking-wide;
|
@apply text-xs font-semibold uppercase tracking-wide;
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +176,7 @@
|
|||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
line-height: var(--line-height-tight);
|
line-height: var(--line-height-tight);
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
@@ -186,7 +192,8 @@
|
|||||||
|
|
||||||
.tool-call-markdown {
|
.tool-call-markdown {
|
||||||
background-color: var(--surface-code);
|
background-color: var(--surface-code);
|
||||||
border: none;
|
/* Keep a visible frame around the scroll viewport (not the content). */
|
||||||
|
border: 1px solid var(--tool-call-border-color);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
@@ -199,6 +206,16 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inner code blocks should not own the frame border; the scroll container does. */
|
||||||
|
.tool-call-markdown .markdown-code-block {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avoid double borders when ANSI output uses .tool-call-content inside tool-call-markdown. */
|
||||||
|
.tool-call-markdown .tool-call-content {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tool-call-markdown-large {
|
.tool-call-markdown-large {
|
||||||
max-height: var(--tool-call-max-height-large, calc(48 * 1.4em));
|
max-height: var(--tool-call-max-height-large, calc(48 * 1.4em));
|
||||||
}
|
}
|
||||||
@@ -216,7 +233,7 @@
|
|||||||
.tool-call-diff-toolbar {
|
.tool-call-diff-toolbar {
|
||||||
@apply flex items-center justify-between gap-3 px-3 py-2;
|
@apply flex items-center justify-between gap-3 px-3 py-2;
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
border-bottom: 1px solid var(--border-base);
|
border-bottom: 1px solid var(--tool-call-border-color);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -231,7 +248,7 @@
|
|||||||
|
|
||||||
.tool-call-diff-toolbar-label {
|
.tool-call-diff-toolbar-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
@@ -242,9 +259,9 @@
|
|||||||
|
|
||||||
.tool-call-diff-mode-button {
|
.tool-call-diff-mode-button {
|
||||||
@apply border text-xs font-semibold px-3 py-1 rounded transition-all duration-150;
|
@apply border text-xs font-semibold px-3 py-1 rounded transition-all duration-150;
|
||||||
border-color: var(--border-base);
|
border-color: var(--tool-call-border-color, var(--border-base));
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-diff-mode-button:hover {
|
.tool-call-diff-mode-button:hover {
|
||||||
@@ -303,7 +320,7 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--tool-call-border-color, var(--border-base));
|
||||||
background-color: var(--surface-code);
|
background-color: var(--surface-code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +329,7 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
background-color: var(--surface-code);
|
background-color: var(--surface-code);
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--tool-call-border-color, var(--border-base));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -388,19 +405,13 @@
|
|||||||
.tool-call-diff-viewer .diff-line-old-num,
|
.tool-call-diff-viewer .diff-line-old-num,
|
||||||
.tool-call-diff-viewer .diff-line-new-num,
|
.tool-call-diff-viewer .diff-line-new-num,
|
||||||
.tool-call-diff-viewer .diff-line-num {
|
.tool-call-diff-viewer .diff-line-num {
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-markdown .markdown-code-block {
|
.tool-call-markdown .markdown-code-block {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.tool-call-markdown .markdown-code-block {
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +419,25 @@
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: auto;
|
z-index: auto;
|
||||||
box-shadow: 0 1px 0 var(--border-base);
|
background-color: var(--code-block-header-bg, var(--surface-secondary));
|
||||||
|
border-bottom: 1px solid var(--tool-call-border-color);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tool output header (language + copy) needs stronger contrast in light mode. */
|
||||||
|
.tool-call-markdown .code-block-language {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-markdown .code-block-copy {
|
||||||
|
border-color: var(--tool-call-border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Plain (non-highlighted) tool output must remain readable in light mode. */
|
||||||
|
.tool-call-markdown .markdown-code-block pre:not(.shiki),
|
||||||
|
.tool-call-markdown .markdown-code-block pre:not(.shiki) code {
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-markdown .markdown-code-block pre {
|
.tool-call-markdown .markdown-code-block pre {
|
||||||
@@ -418,6 +447,14 @@
|
|||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Shiki injects inline background colors; force token surfaces. */
|
||||||
|
.tool-call-markdown pre.shiki,
|
||||||
|
.tool-call-markdown pre.shiki code,
|
||||||
|
.tool-call-markdown .shiki {
|
||||||
|
background: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
.tool-call-markdown::-webkit-scrollbar {
|
.tool-call-markdown::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
@@ -450,7 +487,7 @@
|
|||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-diagnostics {
|
.tool-call-diagnostics {
|
||||||
@@ -458,12 +495,12 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
padding: var(--space-sm) var(--space-md);
|
padding: var(--space-sm) var(--space-md);
|
||||||
border-top: 1px solid var(--border-base);
|
border-top: 1px solid var(--tool-call-border-color);
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-diagnostics-wrapper {
|
.tool-call-diagnostics-wrapper {
|
||||||
border-top: 1px solid var(--border-base);
|
border-top: 1px solid var(--tool-call-border-color);
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
margin-top: var(--space-md);
|
margin-top: var(--space-md);
|
||||||
}
|
}
|
||||||
@@ -472,7 +509,7 @@
|
|||||||
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left;
|
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left;
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--message-tool-border);
|
color: var(--text-primary);
|
||||||
background-color: var(--surface-code);
|
background-color: var(--surface-code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +533,7 @@
|
|||||||
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left;
|
@apply flex items-center gap-2 p-2 w-full border-none cursor-pointer text-left;
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--message-tool-border);
|
color: var(--text-primary);
|
||||||
background-color: var(--surface-code);
|
background-color: var(--surface-code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,7 +552,7 @@
|
|||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--tool-call-border-color, var(--border-base));
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,7 +572,7 @@
|
|||||||
|
|
||||||
.tool-call-diagnostics-caret {
|
.tool-call-diagnostics-caret {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-diagnostics {
|
.tool-call-diagnostics {
|
||||||
@@ -615,7 +652,8 @@
|
|||||||
.tool-call-section pre {
|
.tool-call-section pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-code);
|
||||||
|
border: 1px solid var(--tool-call-border-color);
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
max-height: var(--tool-call-max-height-compact, calc(25 * 1.4em));
|
max-height: var(--tool-call-max-height-compact, calc(25 * 1.4em));
|
||||||
@@ -649,7 +687,7 @@
|
|||||||
|
|
||||||
.tool-call-pending-message {
|
.tool-call-pending-message {
|
||||||
@apply flex items-center gap-2 p-3 text-xs italic;
|
@apply flex items-center gap-2 p-3 text-xs italic;
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-emoji {
|
.tool-call-emoji {
|
||||||
@@ -658,8 +696,8 @@
|
|||||||
|
|
||||||
.tool-call-action-button {
|
.tool-call-action-button {
|
||||||
@apply border text-xs font-semibold px-3 py-1 rounded transition-colors h-8 flex items-center;
|
@apply border text-xs font-semibold px-3 py-1 rounded transition-colors h-8 flex items-center;
|
||||||
border-color: var(--border-base);
|
border-color: var(--tool-call-border-color, var(--border-base));
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,8 +717,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-content {
|
.tool-call-content {
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-code);
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--tool-call-border-color);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
@@ -688,6 +726,7 @@
|
|||||||
line-height: var(--line-height-tight);
|
line-height: var(--line-height-tight);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-content code {
|
.tool-call-content code {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-task-section {
|
.tool-call-task-section {
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--tool-call-border-color, var(--border-base));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
border-bottom: 1px solid var(--border-base);
|
border-bottom: 1px solid var(--tool-call-border-color, var(--border-base));
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
padding: 0.35rem 0.5rem 0.35rem 0.75rem;
|
padding: 0.35rem 0.5rem 0.35rem 0.75rem;
|
||||||
border-left: 2px solid var(--border-base);
|
border-left: 2px solid var(--tool-call-border-color, var(--border-base));
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
.tool-call-todo-item {
|
.tool-call-todo-item {
|
||||||
@apply flex items-start gap-3;
|
@apply flex items-start gap-3;
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--tool-call-border-color, var(--border-base));
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
width: 1.1rem;
|
width: 1.1rem;
|
||||||
height: 1.1rem;
|
height: 1.1rem;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
border: 2px solid var(--border-base);
|
border: 2px solid var(--tool-call-border-color, var(--border-base));
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -132,6 +132,13 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-sidebar-selector-hints {
|
||||||
|
@apply flex flex-wrap items-center w-full text-xs;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
gap: 4px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.session-header-hints {
|
.session-header-hints {
|
||||||
@apply flex-shrink-0;
|
@apply flex-shrink-0;
|
||||||
}
|
}
|
||||||
@@ -482,4 +489,3 @@
|
|||||||
border-color: var(--border-base);
|
border-color: var(--border-base);
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,55 @@
|
|||||||
ring-offset-color: inherit;
|
ring-offset-color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Instance tabs: session-status dots should be lighter/softer than list/status pills. */
|
||||||
|
.tab-base .status-indicator.session-status.session-working {
|
||||||
|
--session-status-dot: color-mix(in oklab, var(--session-status-working-fg) 55%, var(--surface-base));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-base .status-indicator.session-status.session-compacting {
|
||||||
|
--session-status-dot: color-mix(in oklab, var(--session-status-compacting-fg) 55%, var(--surface-base));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-base .status-indicator.session-status.session-idle {
|
||||||
|
--session-status-dot: color-mix(in oklab, var(--session-status-idle-fg) 55%, var(--surface-base));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-base .status-indicator.session-status.session-permission {
|
||||||
|
--session-status-dot: color-mix(in oklab, var(--session-status-permission-fg) 55%, var(--surface-base));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode: keep dots vivid (avoid muddy mixes). */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme]) .tab-base .status-indicator.session-status.session-working {
|
||||||
|
--session-status-dot: var(--session-status-working-fg);
|
||||||
|
}
|
||||||
|
:root:not([data-theme]) .tab-base .status-indicator.session-status.session-compacting {
|
||||||
|
--session-status-dot: var(--session-status-compacting-fg);
|
||||||
|
}
|
||||||
|
:root:not([data-theme]) .tab-base .status-indicator.session-status.session-idle {
|
||||||
|
--session-status-dot: var(--session-status-idle-fg);
|
||||||
|
}
|
||||||
|
:root:not([data-theme]) .tab-base .status-indicator.session-status.session-permission {
|
||||||
|
--session-status-dot: var(--session-status-permission-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .tab-base .status-indicator.session-status.session-working {
|
||||||
|
--session-status-dot: var(--session-status-working-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .tab-base .status-indicator.session-status.session-compacting {
|
||||||
|
--session-status-dot: var(--session-status-compacting-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .tab-base .status-indicator.session-status.session-idle {
|
||||||
|
--session-status-dot: var(--session-status-idle-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .tab-base .status-indicator.session-status.session-permission {
|
||||||
|
--session-status-dot: var(--session-status-permission-fg);
|
||||||
|
}
|
||||||
|
|
||||||
.new-tab-button {
|
.new-tab-button {
|
||||||
@apply inline-flex items-center justify-center w-8 h-8 rounded-md transition-colors;
|
@apply inline-flex items-center justify-center w-8 h-8 rounded-md transition-colors;
|
||||||
background-color: var(--new-tab-bg);
|
background-color: var(--new-tab-bg);
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
:root {
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
/* Surface tokens */
|
/* Surface tokens */
|
||||||
--surface-base: #ffffff;
|
--surface-base: #ffffff;
|
||||||
--surface-secondary: #f5f5f5;
|
--surface-secondary: #f5f5f5;
|
||||||
--surface-muted: #f8f9fa;
|
--surface-muted: #f8fafc;
|
||||||
--surface-code: #f8f8f8;
|
--surface-code: #f1f5f9;
|
||||||
--surface-hover: #e0e0e0;
|
--surface-hover: #e0e0e0;
|
||||||
|
|
||||||
/* Border tokens */
|
/* Border tokens */
|
||||||
--border-base: #e0e0e0;
|
--border-base: #e0e0e0;
|
||||||
--border-secondary: #e0e0e0;
|
--border-secondary: #e0e0e0;
|
||||||
--border-muted: #e0e0e0;
|
--border-muted: #e0e0e0;
|
||||||
|
--border-strong: color-mix(in oklab, var(--border-base) 62%, var(--text-primary));
|
||||||
|
|
||||||
/* Text tokens */
|
/* Text tokens */
|
||||||
--text-primary: #1a1a1a;
|
--text-primary: #111827;
|
||||||
--text-secondary: #666666;
|
--text-secondary: #334155;
|
||||||
--text-muted: #666666;
|
--text-muted: #475569;
|
||||||
--text-inverted: #ffffff;
|
--text-inverted: #ffffff;
|
||||||
|
|
||||||
/* Accent tokens */
|
/* Accent tokens */
|
||||||
@@ -27,13 +29,13 @@
|
|||||||
--status-warning: #ff9800;
|
--status-warning: #ff9800;
|
||||||
|
|
||||||
/* Message-specific tokens */
|
/* Message-specific tokens */
|
||||||
--message-user-bg: var(--surface-secondary);
|
--message-user-bg: color-mix(in oklab, var(--surface-secondary) 88%, var(--message-user-border));
|
||||||
--message-user-border: #2196f3;
|
--message-user-border: #2196f3;
|
||||||
--message-assistant-bg: var(--message-tool-bg);
|
--message-assistant-bg: var(--message-tool-bg);
|
||||||
--message-assistant-border: #f59e0b;
|
--message-assistant-border: #f59e0b;
|
||||||
|
|
||||||
--message-tool-bg: #f8f9fa;
|
--message-tool-bg: #eef2f7;
|
||||||
--message-tool-border: #6c757d;
|
--message-tool-border: #64748b;
|
||||||
|
|
||||||
/* Session list selection tints */
|
/* Session list selection tints */
|
||||||
--session-user-active-bg: color-mix(in oklab, var(--surface-secondary) 85%, var(--message-user-border));
|
--session-user-active-bg: color-mix(in oklab, var(--surface-secondary) 85%, var(--message-user-border));
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
--attachment-chip-ring: rgba(0, 102, 255, 0.1);
|
--attachment-chip-ring: rgba(0, 102, 255, 0.1);
|
||||||
--badge-neutral-bg: rgba(0, 102, 255, 0.05);
|
--badge-neutral-bg: rgba(0, 102, 255, 0.05);
|
||||||
--badge-neutral-text: #0066ff;
|
--badge-neutral-text: #0066ff;
|
||||||
|
--badge-success-bg: rgba(76, 175, 80, 0.12);
|
||||||
--status-ready-fg: #16a34a;
|
--status-ready-fg: #16a34a;
|
||||||
--status-ready-bg: rgba(34, 197, 94, 0.1);
|
--status-ready-bg: rgba(34, 197, 94, 0.1);
|
||||||
--status-starting-fg: #ca8a04;
|
--status-starting-fg: #ca8a04;
|
||||||
@@ -71,11 +74,15 @@
|
|||||||
--folder-card-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
--folder-card-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||||
--folder-card-radius: 16px;
|
--folder-card-radius: 16px;
|
||||||
--dropdown-highlight-bg: rgba(0, 102, 255, 0.1);
|
--dropdown-highlight-bg: rgba(0, 102, 255, 0.1);
|
||||||
--dropdown-highlight-text: var(--text-inverted);
|
--dropdown-highlight-text: var(--text-primary);
|
||||||
--selection-highlight-bg: rgba(0, 102, 255, 0.12);
|
--selection-highlight-bg: rgba(0, 102, 255, 0.12);
|
||||||
--selection-highlight-strong-bg: rgba(0, 102, 255, 0.18);
|
--selection-highlight-strong-bg: rgba(0, 102, 255, 0.18);
|
||||||
--overlay-scrim: rgba(0, 0, 0, 0.5);
|
--overlay-scrim: rgba(0, 0, 0, 0.5);
|
||||||
--scroll-elevation-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
|
--scroll-elevation-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
|
||||||
|
--panel-shadow: 0 6px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
--panel-shadow-strong: 0 12px 32px rgba(0, 0, 0, 0.18);
|
||||||
|
--popover-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
|
||||||
|
--code-block-header-bg: color-mix(in oklab, var(--surface-secondary) 78%, var(--text-primary));
|
||||||
--message-error-bg: rgba(244, 67, 54, 0.1);
|
--message-error-bg: rgba(244, 67, 54, 0.1);
|
||||||
--message-error-bg-strong: rgba(244, 67, 54, 0.15);
|
--message-error-bg-strong: rgba(244, 67, 54, 0.15);
|
||||||
--danger-soft-bg: rgba(239, 68, 68, 0.1);
|
--danger-soft-bg: rgba(239, 68, 68, 0.1);
|
||||||
@@ -86,6 +93,19 @@
|
|||||||
--log-level-default: var(--text-primary);
|
--log-level-default: var(--text-primary);
|
||||||
--focus-ring-color: var(--accent-primary);
|
--focus-ring-color: var(--accent-primary);
|
||||||
--focus-ring-offset: var(--surface-base);
|
--focus-ring-offset: var(--surface-base);
|
||||||
|
--control-ghost-bg: color-mix(in oklab, var(--text-primary) 6%, transparent);
|
||||||
|
--status-success-ring: color-mix(in oklab, var(--status-success) 45%, transparent);
|
||||||
|
--border-critical: var(--status-error);
|
||||||
|
|
||||||
|
/* Message timeline active segment (light theme should be a light tint). */
|
||||||
|
--timeline-segment-active-bg: #b7e6d6;
|
||||||
|
--timeline-segment-active-text: #032f23;
|
||||||
|
--timeline-segment-active-ring: inset 0 0 0 1px rgba(3, 47, 35, 0.28);
|
||||||
|
|
||||||
|
--button-danger-bg: color-mix(in oklab, var(--status-error) 85%, var(--surface-base));
|
||||||
|
--button-danger-hover-bg: color-mix(in oklab, var(--status-error) 90%, var(--surface-base));
|
||||||
|
--button-danger-active-bg: color-mix(in oklab, var(--status-error) 95%, var(--surface-base));
|
||||||
|
--button-danger-text: #ffffff;
|
||||||
--kbd-bg: var(--surface-secondary);
|
--kbd-bg: var(--surface-secondary);
|
||||||
--kbd-border: var(--border-base);
|
--kbd-border: var(--border-base);
|
||||||
--kbd-text: var(--text-primary);
|
--kbd-text: var(--text-primary);
|
||||||
@@ -151,7 +171,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root:not([data-theme]) {
|
||||||
|
color-scheme: dark;
|
||||||
/* Surface tokens */
|
/* Surface tokens */
|
||||||
--surface-base: #1a1a1a;
|
--surface-base: #1a1a1a;
|
||||||
--surface-secondary: #2a2a2a;
|
--surface-secondary: #2a2a2a;
|
||||||
@@ -163,6 +184,7 @@
|
|||||||
--border-base: #3a3a3a;
|
--border-base: #3a3a3a;
|
||||||
--border-secondary: #3a3a3a;
|
--border-secondary: #3a3a3a;
|
||||||
--border-muted: #3a3a3a;
|
--border-muted: #3a3a3a;
|
||||||
|
--border-strong: var(--border-base);
|
||||||
|
|
||||||
/* Text tokens */
|
/* Text tokens */
|
||||||
--text-primary: #cfd4dc;
|
--text-primary: #cfd4dc;
|
||||||
@@ -206,6 +228,7 @@
|
|||||||
--attachment-chip-ring: rgba(0, 128, 255, 0.2);
|
--attachment-chip-ring: rgba(0, 128, 255, 0.2);
|
||||||
--badge-neutral-bg: rgba(0, 128, 255, 0.15);
|
--badge-neutral-bg: rgba(0, 128, 255, 0.15);
|
||||||
--badge-neutral-text: #0080ff;
|
--badge-neutral-text: #0080ff;
|
||||||
|
--badge-success-bg: rgba(76, 175, 80, 0.22);
|
||||||
--status-ready-fg: #22c55e;
|
--status-ready-fg: #22c55e;
|
||||||
--status-ready-bg: rgba(34, 197, 94, 0.2);
|
--status-ready-bg: rgba(34, 197, 94, 0.2);
|
||||||
--status-starting-fg: #facc15;
|
--status-starting-fg: #facc15;
|
||||||
@@ -225,6 +248,15 @@
|
|||||||
--kbd-bg: var(--surface-secondary);
|
--kbd-bg: var(--surface-secondary);
|
||||||
--kbd-border: var(--border-base);
|
--kbd-border: var(--border-base);
|
||||||
--kbd-text: var(--text-primary);
|
--kbd-text: var(--text-primary);
|
||||||
|
--panel-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
|
||||||
|
--panel-shadow-strong: 0 12px 32px rgba(0, 0, 0, 0.45);
|
||||||
|
--popover-shadow: 0 16px 40px rgba(0, 0, 0, 0.55);
|
||||||
|
--code-block-header-bg: var(--surface-secondary);
|
||||||
|
--border-critical: var(--status-error);
|
||||||
|
--timeline-segment-active-bg: #0f5b44;
|
||||||
|
--timeline-segment-active-text: #ffffff;
|
||||||
|
--timeline-segment-active-ring: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
|
||||||
|
--button-danger-text: #ffffff;
|
||||||
--button-primary-bg: #3f3f46;
|
--button-primary-bg: #3f3f46;
|
||||||
--button-primary-hover-bg: #52525b;
|
--button-primary-hover-bg: #52525b;
|
||||||
--button-primary-text: #f5f6f8;
|
--button-primary-text: #f5f6f8;
|
||||||
@@ -306,6 +338,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
/* Surface tokens */
|
/* Surface tokens */
|
||||||
--surface-base: #1a1a1a;
|
--surface-base: #1a1a1a;
|
||||||
--surface-secondary: #2a2a2a;
|
--surface-secondary: #2a2a2a;
|
||||||
@@ -317,6 +350,7 @@
|
|||||||
--border-base: #3a3a3a;
|
--border-base: #3a3a3a;
|
||||||
--border-secondary: #3a3a3a;
|
--border-secondary: #3a3a3a;
|
||||||
--border-muted: #3a3a3a;
|
--border-muted: #3a3a3a;
|
||||||
|
--border-strong: var(--border-base);
|
||||||
|
|
||||||
/* Text tokens */
|
/* Text tokens */
|
||||||
--text-primary: #cfd4dc;
|
--text-primary: #cfd4dc;
|
||||||
@@ -359,6 +393,7 @@
|
|||||||
--attachment-chip-ring: rgba(0, 128, 255, 0.2);
|
--attachment-chip-ring: rgba(0, 128, 255, 0.2);
|
||||||
--badge-neutral-bg: rgba(0, 128, 255, 0.15);
|
--badge-neutral-bg: rgba(0, 128, 255, 0.15);
|
||||||
--badge-neutral-text: #0080ff;
|
--badge-neutral-text: #0080ff;
|
||||||
|
--badge-success-bg: rgba(76, 175, 80, 0.22);
|
||||||
--status-ready-fg: #22c55e;
|
--status-ready-fg: #22c55e;
|
||||||
--status-ready-bg: rgba(34, 197, 94, 0.2);
|
--status-ready-bg: rgba(34, 197, 94, 0.2);
|
||||||
--status-starting-fg: #facc15;
|
--status-starting-fg: #facc15;
|
||||||
@@ -379,6 +414,15 @@
|
|||||||
--selection-highlight-strong-bg: rgba(0, 128, 255, 0.28);
|
--selection-highlight-strong-bg: rgba(0, 128, 255, 0.28);
|
||||||
--overlay-scrim: rgba(0, 0, 0, 0.6);
|
--overlay-scrim: rgba(0, 0, 0, 0.6);
|
||||||
--scroll-elevation-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
|
--scroll-elevation-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
|
||||||
|
--panel-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
|
||||||
|
--panel-shadow-strong: 0 12px 32px rgba(0, 0, 0, 0.45);
|
||||||
|
--popover-shadow: 0 16px 40px rgba(0, 0, 0, 0.55);
|
||||||
|
--code-block-header-bg: var(--surface-secondary);
|
||||||
|
--border-critical: var(--status-error);
|
||||||
|
--timeline-segment-active-bg: #0f5b44;
|
||||||
|
--timeline-segment-active-text: #ffffff;
|
||||||
|
--timeline-segment-active-ring: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
|
||||||
|
--button-danger-text: #ffffff;
|
||||||
--message-error-bg: rgba(244, 67, 54, 0.12);
|
--message-error-bg: rgba(244, 67, 54, 0.12);
|
||||||
--message-error-bg-strong: rgba(244, 67, 54, 0.2);
|
--message-error-bg-strong: rgba(244, 67, 54, 0.2);
|
||||||
--danger-soft-bg: rgba(244, 67, 54, 0.16);
|
--danger-soft-bg: rgba(244, 67, 54, 0.16);
|
||||||
@@ -392,6 +436,18 @@
|
|||||||
--kbd-bg: var(--surface-secondary);
|
--kbd-bg: var(--surface-secondary);
|
||||||
--kbd-border: var(--border-base);
|
--kbd-border: var(--border-base);
|
||||||
--kbd-text: var(--text-primary);
|
--kbd-text: var(--text-primary);
|
||||||
|
--button-primary-bg: #3f3f46;
|
||||||
|
--button-primary-hover-bg: #52525b;
|
||||||
|
--button-primary-text: #f5f6f8;
|
||||||
|
--tab-active-bg: #3f3f46;
|
||||||
|
--tab-active-hover-bg: #52525b;
|
||||||
|
--tab-active-text: #f5f6f8;
|
||||||
|
--tab-inactive-bg: #2a2a31;
|
||||||
|
--tab-inactive-hover-bg: #3f3f46;
|
||||||
|
--tab-inactive-text: #d4d4d8;
|
||||||
|
--new-tab-bg: #3f3f46;
|
||||||
|
--new-tab-hover-bg: #52525b;
|
||||||
|
--new-tab-text: #f5f6f8;
|
||||||
|
|
||||||
/* Layout & spacing tokens */
|
/* Layout & spacing tokens */
|
||||||
--space-2xs: 2px;
|
--space-2xs: 2px;
|
||||||
|
|||||||
Reference in New Issue
Block a user