Compare commits

...

28 Commits

Author SHA1 Message Date
Shantur Rathore
0368fe8248 fix(ci): avoid bash globstar on macOS 2026-02-24 07:29:26 +00:00
Shantur Rathore
b970281fa7 chore(electron): use cross-env for dev log level scripts
Make dev:info/dev:debug/dev:trace work on Windows by setting CLI_LOG_LEVEL via cross-env.
2026-02-24 00:19:39 +00:00
Shantur Rathore
8e5a7fc213 fix(electron): make dev CLI log level configurable
Use CLI_LOG_LEVEL when launching the server in desktop dev and add dev:info/dev:debug/dev:trace scripts with dev defaulting to info.
2026-02-24 00:09:49 +00:00
Shantur Rathore
15f362e8b5 Bump v0.11.5 2026-02-23 23:55:52 +00:00
Shantur Rathore
7bbd0a1787 Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev 2026-02-23 23:55:32 +00:00
Shantur Rathore
f8aae56728 Merge pull request #190 from VooDisss/issue-187
fix(ui): prevent timeline auto-scroll when removing badges (#189)
2026-02-23 21:50:56 +00:00
Shantur Rathore
027d7fc97d fix(ui): load shiki languages from marked tokens 2026-02-23 18:39:21 +00:00
Shantur Rathore
e90aef4b3c fix(ui): stack instance header under 1024px 2026-02-23 18:36:24 +00:00
Shantur Rathore
e4e89008b2 Merge pull request #199 from NeuralNomadsAI/codenomad/issue-198
CI: rezip Electron macOS artifacts with ditto + validate codesign
2026-02-23 08:58:56 +00:00
Shantur Rathore
90baefbb7e fix(ci): rezip Electron macOS zips with ditto
Add a codesign verify step on extracted artifacts to catch signature/resource mismatches before upload.
2026-02-23 08:54:57 +00:00
Shantur Rathore
1c138f4489 Merge pull request #197 from VooDisss/issue-195
fix: Use legacy diff algorithm for better large file performance
2026-02-23 08:27:11 +00:00
VooDisss
d36e568ed0 fix: Use legacy diff algorithm for better large file performance
- Set diffAlgorithm to 'legacy' for Monaco DiffEditor
- Add maxComputationTime of 10s to avoid UI freeze on huge files

This addresses the issue where sessions with large JSON files (50k-100k+ lines)
would cause the UI to freeze. The 'legacy' algorithm is faster than 'advanced'
for large files, similar to VSCode's workaround for the same issue.

See: https://github.com/microsoft/vscode/issues/184037
2026-02-23 02:30:44 +02:00
Shantur Rathore
d6462ef524 Min version 0.11.4 2026-02-22 17:32:28 +00:00
Shantur Rathore
a06884ebce Bump to v0.11.4 2026-02-22 16:53:51 +00:00
Shantur Rathore
62bd88f6a4 chore(plugin): Upgrade dependency version 2026-02-22 16:48:49 +00:00
Shantur Rathore
6479561779 fix(ui): auto-expand session thread when child starts working 2026-02-22 16:47:04 +00:00
Shantur Rathore
635237c258 fix(ui): render task prompt consistently while running 2026-02-22 08:58:39 +00:00
Shantur Rathore
33f0aa5714 ci: run dev prerelease nightly
Replace dev push builds with nightly schedule that only runs when dev head advances; still runs on manual dispatch. Plumb a ref input through reusable workflows so scheduled runs build the dev commit.
2026-02-20 13:58:32 +00:00
Shantur Rathore
7ca6285d58 Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev 2026-02-20 13:49:03 +00:00
Shantur Rathore
14c60fef6c Merge pull request #192 from VooDisss/issue-144
[QOL] Add informational tooltips to Status Panel sections
2026-02-20 13:47:11 +00:00
Shantur Rathore
336de6a19e fix(i18n): polish Status panel tooltip translations 2026-02-20 13:46:43 +00:00
Shantur Rathore
377c8e2249 Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev 2026-02-20 13:31:52 +00:00
VooDisss
697dea21f8 Add informational tooltips to Status Panel sections 2026-02-20 14:09:54 +02:00
VooDisss
96fe1b86dd fix(ui): prevent timeline auto-scroll when removing badges 2026-02-20 12:33:52 +02:00
Shantur Rathore
5fabf286e8 ui: restyle command palette button 2026-02-20 00:32:44 +00:00
Shantur Rathore
e8947d61b1 ui: emphasize command palette button 2026-02-20 00:32:39 +00:00
Shantur Rathore
1ccd14eae8 ui: use Check icon for completed status 2026-02-20 00:32:27 +00:00
Shantur Rathore
b162764ccb ui: use lucide status icons for tool calls 2026-02-20 00:32:15 +00:00
33 changed files with 416 additions and 77 deletions

View File

@@ -3,6 +3,11 @@ name: Build and Upload Binaries
on:
workflow_call:
inputs:
ref:
description: "Git ref (branch, tag, or SHA) to build from"
required: false
default: ""
type: string
version:
description: "Version to apply to workspace packages (release builds)"
required: false
@@ -45,6 +50,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -65,6 +72,81 @@ jobs:
- name: Build macOS binaries (Electron)
run: npm run build:mac --workspace @neuralnomads/codenomad-electron-app
- name: Repackage Electron macOS zips (ditto)
shell: bash
run: |
set -euo pipefail
# Prefer the workflow-provided version; fall back to package.json.
VERSION_TO_USE="${VERSION:-}"
if [ -z "$VERSION_TO_USE" ]; then
VERSION_TO_USE=$(node -p "require('./packages/electron-app/package.json').version")
fi
release_root="packages/electron-app/release"
# macOS GitHub runners ship /bin/bash 3.2 which doesn't support `shopt -s globstar`.
# Use find to locate built app bundles instead of ** globs.
apps=()
while IFS= read -r -d '' app; do
apps+=("$app")
done < <(find "$release_root" -type d -name 'CodeNomad.app' -print0)
if [ "${#apps[@]}" -eq 0 ]; then
echo "No CodeNomad.app found under $release_root" >&2
exit 1
fi
for app in "${apps[@]}"; do
bundle_dir=$(basename "$(dirname "$app")")
arch="x64"
if [[ "$bundle_dir" == *"arm64"* ]]; then
arch="arm64"
fi
out_zip="$release_root/CodeNomad-${VERSION_TO_USE}-mac-${arch}.zip"
rm -f "$out_zip"
echo "ditto -ck: $app -> $out_zip"
ditto -ck --sequesterRsrc --keepParent "$app" "$out_zip"
done
- name: Validate Electron macOS codesign (unzipped)
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
zips=(packages/electron-app/release/CodeNomad-*-mac-*.zip)
if [ "${#zips[@]}" -eq 0 ]; then
echo "No Electron macOS zip artifacts found to validate" >&2
exit 1
fi
for zip in "${zips[@]}"; do
echo "Validating codesign for: $zip"
extract_dir="$tmp_dir/$(basename "$zip" .zip)"
mkdir -p "$extract_dir"
# Use ditto for extraction as well to preserve bundle metadata.
ditto -x -k "$zip" "$extract_dir"
app_path=""
for candidate in "$extract_dir"/*.app "$extract_dir"/*/*.app; do
if [ -d "$candidate" ]; then
app_path="$candidate"
break
fi
done
if [ -z "$app_path" ]; then
echo "No .app found after extracting $zip" >&2
exit 1
fi
codesign --verify --deep --strict --verbose=2 "$app_path"
done
- name: Upload release assets
if: ${{ inputs.upload && inputs.tag != '' }}
run: |
@@ -85,6 +167,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -124,6 +208,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -164,6 +250,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -237,6 +325,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -310,6 +400,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -388,6 +480,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -490,6 +584,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
@@ -587,6 +683,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -1,12 +1,13 @@
name: Develop Pre-Release
on:
push:
branches:
- dev
schedule:
# Nightly build of dev (only if dev has new commits)
- cron: "0 1 * * *"
workflow_dispatch:
permissions:
actions: read
id-token: write
contents: write
@@ -15,25 +16,63 @@ concurrency:
cancel-in-progress: true
jobs:
prepare:
gate:
runs-on: ubuntu-latest
outputs:
version_suffix: ${{ steps.vars.outputs.version_suffix }}
run: ${{ steps.gate.outputs.run }}
dev_sha: ${{ steps.gate.outputs.dev_sha }}
version_suffix: ${{ steps.gate.outputs.version_suffix }}
steps:
- name: Compute version suffix
id: vars
- name: Decide whether to run
id: gate
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
SHA8="${GITHUB_SHA::8}"
api() {
curl -sS \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$1"
}
DEV_SHA=$(api "https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/heads/dev" | jq -r '.object.sha')
if [ -z "$DEV_SHA" ] || [ "$DEV_SHA" = "null" ]; then
echo "Failed to resolve dev head SHA" >&2
exit 1
fi
DATE=$(date -u +%Y%m%d)
echo "version_suffix=-dev-${DATE}-${SHA8}" >> "$GITHUB_OUTPUT"
SHA8="${DEV_SHA::8}"
VERSION_SUFFIX="-dev-${DATE}-${SHA8}"
SHOULD_RUN="false"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
SHOULD_RUN="true"
else
# Nightly: only run if dev has advanced since last successful dev-release build.
LAST_SHA=$(api "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/dev-release.yml/runs?branch=dev&status=success&per_page=1" | jq -r '.workflow_runs[0].head_sha // empty')
if [ -z "${LAST_SHA}" ]; then
SHOULD_RUN="true"
elif [ "${LAST_SHA}" != "${DEV_SHA}" ]; then
SHOULD_RUN="true"
fi
fi
echo "run=${SHOULD_RUN}" >> "$GITHUB_OUTPUT"
echo "dev_sha=${DEV_SHA}" >> "$GITHUB_OUTPUT"
echo "version_suffix=${VERSION_SUFFIX}" >> "$GITHUB_OUTPUT"
prerelease:
needs: prepare
needs: gate
if: ${{ needs.gate.outputs.run == 'true' }}
uses: ./.github/workflows/reusable-release.yml
with:
version_suffix: ${{ needs.prepare.outputs.version_suffix }}
ref: ${{ needs.gate.outputs.dev_sha }}
version_suffix: ${{ needs.gate.outputs.version_suffix }}
npm_package_name: "@neuralnomads/codenomad-dev"
dist_tag: latest
prerelease: true

View File

@@ -19,6 +19,10 @@ on:
type: string
workflow_call:
inputs:
ref:
required: false
default: ""
type: string
version:
required: true
type: string
@@ -46,6 +50,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -1,7 +1,13 @@
name: Release UI
on:
workflow_call: {}
workflow_call:
inputs:
ref:
description: "Git ref (branch, tag, or SHA) to build from"
required: false
default: ""
type: string
workflow_dispatch: {}
permissions:
@@ -18,6 +24,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -3,6 +3,11 @@ name: Reusable Release
on:
workflow_call:
inputs:
ref:
description: "Git ref (branch, tag, or SHA) to build from"
required: false
default: ""
type: string
version_suffix:
description: "Suffix appended to package.json version"
required: false
@@ -46,6 +51,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -84,6 +91,7 @@ jobs:
needs: prepare-release
uses: ./.github/workflows/build-and-upload.yml
with:
ref: ${{ inputs.ref || github.ref }}
version: ${{ needs.prepare-release.outputs.version }}
tag: ${{ needs.prepare-release.outputs.tag }}
release_name: ${{ needs.prepare-release.outputs.release_name }}
@@ -95,6 +103,8 @@ jobs:
permissions:
contents: read
uses: ./.github/workflows/release-ui.yml
with:
ref: ${{ inputs.ref || github.ref }}
secrets: inherit
publish-server:
@@ -103,6 +113,7 @@ jobs:
- build-and-upload
uses: ./.github/workflows/manual-npm-publish.yml
with:
ref: ${{ inputs.ref || github.ref }}
version: ${{ needs.prepare-release.outputs.version }}
dist_tag: ${{ inputs.dist_tag }}
package_name: ${{ inputs.npm_package_name }}

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "codenomad-workspace",
"version": "0.11.3",
"version": "0.11.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codenomad-workspace",
"version": "0.11.3",
"version": "0.11.5",
"license": "MIT",
"dependencies": {
"7zip-bin": "^5.2.0",
@@ -11985,7 +11985,7 @@
},
"packages/electron-app": {
"name": "@neuralnomads/codenomad-electron-app",
"version": "0.11.3",
"version": "0.11.5",
"license": "MIT",
"dependencies": {
"@codenomad/ui": "file:../ui",
@@ -12021,7 +12021,7 @@
},
"packages/server": {
"name": "@neuralnomads/codenomad",
"version": "0.11.3",
"version": "0.11.5",
"license": "MIT",
"dependencies": {
"@fastify/cors": "^8.5.0",
@@ -12062,7 +12062,7 @@
},
"packages/tauri-app": {
"name": "@codenomad/tauri-app",
"version": "0.11.3",
"version": "0.11.5",
"license": "MIT",
"devDependencies": {
"@tauri-apps/cli": "^2.9.4"
@@ -12070,7 +12070,7 @@
},
"packages/ui": {
"name": "@codenomad/ui",
"version": "0.11.3",
"version": "0.11.5",
"license": "MIT",
"dependencies": {
"@git-diff-view/solid": "^0.0.8",

View File

@@ -1,6 +1,6 @@
{
"name": "codenomad-workspace",
"version": "0.11.3",
"version": "0.11.5",
"private": true,
"description": "CodeNomad monorepo workspace",
"license": "MIT",

View File

@@ -1,4 +1,4 @@
{
"minServerVersion": "0.11.1",
"minServerVersion": "0.11.4",
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
}

View File

@@ -431,7 +431,9 @@ export class CliProcessManager extends EventEmitter {
if (options.dev) {
const devServer = process.env.VITE_DEV_SERVER_URL || process.env.ELECTRON_RENDERER_URL || "http://localhost:3000"
args.push("--ui-dev-server", devServer, "--log-level", "debug")
const rawLogLevel = (process.env.CLI_LOG_LEVEL ?? "info").trim()
const logLevel = rawLogLevel.length > 0 ? rawLogLevel.toLowerCase() : "info"
args.push("--ui-dev-server", devServer, "--log-level", logLevel)
}
return args

View File

@@ -1,6 +1,6 @@
{
"name": "@neuralnomads/codenomad-electron-app",
"version": "0.11.3",
"version": "0.11.5",
"description": "CodeNomad - AI coding assistant",
"license": "MIT",
"author": {
@@ -15,7 +15,10 @@
},
"homepage": "https://github.com/NeuralNomadsAI/CodeNomad",
"scripts": {
"dev": "electron-vite dev",
"dev": "npm run dev:info",
"dev:info": "cross-env CLI_LOG_LEVEL=info electron-vite dev",
"dev:debug": "cross-env CLI_LOG_LEVEL=debug electron-vite dev",
"dev:trace": "cross-env CLI_LOG_LEVEL=trace electron-vite dev",
"dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts",
"build": "electron-vite build",
"typecheck": "tsc --noEmit -p tsconfig.json",
@@ -42,6 +45,7 @@
"devDependencies": {
"7zip-bin": "^5.2.0",
"app-builder-bin": "^4.2.0",
"cross-env": "^7.0.3",
"electron": "39.0.0",
"electron-builder": "^24.0.0",
"electron-vite": "4.0.1",

View File

@@ -4,6 +4,6 @@
"private": true,
"license": "MIT",
"dependencies": {
"@opencode-ai/plugin": "1.2.6"
"@opencode-ai/plugin": "1.2.10"
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@neuralnomads/codenomad",
"version": "0.11.3",
"version": "0.11.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@neuralnomads/codenomad",
"version": "0.11.3",
"version": "0.11.5",
"dependencies": {
"@fastify/cors": "^8.5.0",
"@fastify/reply-from": "^9.8.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@neuralnomads/codenomad",
"version": "0.11.3",
"version": "0.11.5",
"description": "CodeNomad Server",
"license": "MIT",
"author": {

View File

@@ -1,6 +1,6 @@
{
"name": "@codenomad/tauri-app",
"version": "0.11.3",
"version": "0.11.5",
"private": true,
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@codenomad/ui",
"version": "0.11.3",
"version": "0.11.5",
"private": true,
"license": "MIT",
"type": "module",

View File

@@ -61,6 +61,11 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
// Keep enough gutter space so unified diffs don't overlap `+`/`-` markers.
lineNumbersMinChars: 4,
lineDecorationsWidth: 12,
// Use legacy diff algorithm for better performance with large files
// See: https://github.com/microsoft/vscode/issues/184037
diffAlgorithm: "legacy",
// Limit computation time to avoid freezing on large files
maxComputationTime: 10000,
})
setReady(true)

View File

@@ -115,6 +115,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
const desktopQuery = useMediaQuery("(min-width: 1280px)")
const tabletQuery = useMediaQuery("(min-width: 768px)")
const compactHeaderQuery = useMediaQuery("(max-width: 1024px)")
const layoutMode = createMemo<LayoutMode>(() => {
if (desktopQuery()) return "desktop"
@@ -123,6 +124,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
})
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
const compactHeaderLayout = createMemo(() => isPhoneLayout() || compactHeaderQuery())
const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout())
const compactPromptLayout = createMemo(() => layoutMode() !== "desktop")
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
@@ -596,7 +598,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
<Show
when={!isPhoneLayout()}
when={!compactHeaderLayout()}
fallback={
<div class="flex flex-col w-full gap-1.5">
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
@@ -625,7 +627,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<div class="flex flex-wrap items-center justify-center gap-1">
<button
type="button"
class="connection-status-button px-2 py-0.5 text-xs"
class="connection-status-button command-palette-button"
onClick={handleCommandPaletteClick}
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
style={{ flex: "0 0 auto", width: "auto" }}
@@ -634,8 +636,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
</button>
<span class="connection-status-shortcut-hint kbd-hint">
<Kbd shortcut="cmd+shift+p" />
</span>
</div>
</span>
</div>
<div class="flex-1 flex items-center justify-center min-w-0">
<span
@@ -646,7 +648,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
</span>
</div>
<Show when={!props.mobileFullscreenMode}>
<Show when={isPhoneLayout() && !props.mobileFullscreenMode}>
<IconButton
color="inherit"
onClick={props.onEnterMobileFullscreen}
@@ -670,16 +672,18 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
{rightAppBarButtonIcon()}
</IconButton>
</Show>
</div>
</div>
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
<ContextMeter
usedTokens={tokenStats().used}
availableTokens={tokenStats().avail}
formatTokens={formatTokenTotal}
usedLabel={t("instanceShell.metrics.usedLabel")}
availableLabel={t("instanceShell.metrics.availableLabel")}
/>
<Show when={!showingInfoView()}>
<ContextMeter
usedTokens={tokenStats().used}
availableTokens={tokenStats().avail}
formatTokens={formatTokenTotal}
usedLabel={t("instanceShell.metrics.usedLabel")}
availableLabel={t("instanceShell.metrics.availableLabel")}
/>
</Show>
</div>
</div>
}
@@ -721,7 +725,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<div class="session-toolbar-center flex items-center justify-center gap-2 min-w-[160px]">
<button
type="button"
class="connection-status-button px-2 py-0.5 text-xs"
class="connection-status-button command-palette-button"
onClick={handleCommandPaletteClick}
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
style={{ flex: "0 0 auto", width: "auto" }}

View File

@@ -1,8 +1,9 @@
import { For, Show, type Accessor, type Component } from "solid-js"
import type { ToolState } from "@opencode-ai/sdk"
import { Accordion } from "@kobalte/core"
import { Tooltip } from "@kobalte/core/tooltip"
import { ChevronDown, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
import { ChevronDown, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
import type { Instance } from "../../../../../types/instance"
import type { BackgroundProcess } from "../../../../../../../server/src/api-types"
@@ -206,21 +207,25 @@ const StatusTab: Component<StatusTabProps> = (props) => {
{
id: "session-changes",
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
tooltipKey: "instanceShell.rightPanel.sections.sessionChanges.tooltip",
render: renderStatusSessionChanges,
},
{
id: "plan",
labelKey: "instanceShell.rightPanel.sections.plan",
tooltipKey: "instanceShell.rightPanel.sections.plan.tooltip",
render: renderPlanSectionContent,
},
{
id: "background-processes",
labelKey: "instanceShell.rightPanel.sections.backgroundProcesses",
tooltipKey: "instanceShell.rightPanel.sections.backgroundProcesses.tooltip",
render: renderBackgroundProcesses,
},
{
id: "mcp",
labelKey: "instanceShell.rightPanel.sections.mcp",
tooltipKey: "instanceShell.rightPanel.sections.mcp.tooltip",
render: () => (
<InstanceServiceStatus
initialInstance={props.instance}
@@ -233,6 +238,7 @@ const StatusTab: Component<StatusTabProps> = (props) => {
{
id: "lsp",
labelKey: "instanceShell.rightPanel.sections.lsp",
tooltipKey: "instanceShell.rightPanel.sections.lsp.tooltip",
render: () => (
<InstanceServiceStatus
initialInstance={props.instance}
@@ -245,6 +251,7 @@ const StatusTab: Component<StatusTabProps> = (props) => {
{
id: "plugins",
labelKey: "instanceShell.rightPanel.sections.plugins",
tooltipKey: "instanceShell.rightPanel.sections.plugins.tooltip",
render: () => (
<InstanceServiceStatus
initialInstance={props.instance}
@@ -276,7 +283,23 @@ const StatusTab: Component<StatusTabProps> = (props) => {
<Accordion.Item value={section.id} class="right-panel-accordion-item">
<Accordion.Header>
<Accordion.Trigger class="right-panel-accordion-trigger">
<span>{props.t(section.labelKey)}</span>
<span class="section-left">
<Tooltip openDelay={200} gutter={4} placement="top">
<Tooltip.Trigger
class="section-info-trigger"
aria-label={props.t(section.tooltipKey)}
onClick={(e) => e.stopPropagation()}
>
<Info class="section-info-icon" />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content class="section-info-tooltip">
{props.t(section.tooltipKey)}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip>
<span class="section-label">{props.t(section.labelKey)}</span>
</span>
<ChevronDown
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
/>

View File

@@ -51,7 +51,7 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
<div class="connection-status-shortcut-action">
<button
type="button"
class="connection-status-button"
class="connection-status-button command-palette-button"
onClick={props.onCommandPalette}
aria-label={t("messageListHeader.commandPalette.ariaLabel")}
>

View File

@@ -1,4 +1,4 @@
import { For, Show, createEffect, createMemo, createSignal, onCleanup, type Component } from "solid-js"
import { For, Show, createEffect, createMemo, createSignal, onCleanup, on, untrack, type Component } from "solid-js"
import MessagePreview from "./message-preview"
import { messageStoreBus } from "../stores/message-v2/bus"
import type { ClientPart } from "../types/message"
@@ -350,11 +350,9 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
clearCloseTimer()
})
createEffect(() => {
const activeId = props.activeMessageId
createEffect(on(() => props.activeMessageId, (activeId) => {
if (!activeId) return
const targetSegment = props.segments.find((segment) => segment.messageId === activeId)
const targetSegment = untrack(() => props.segments).find((segment) => segment.messageId === activeId)
if (!targetSegment) return
const element = buttonRefs.get(targetSegment.id)
if (!element) return
@@ -366,7 +364,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
window.clearTimeout(timer)
}
})
})
}))
createEffect(() => {
const element = tooltipElement()

View File

@@ -1,5 +1,5 @@
import { createSignal, Show, createEffect, createMemo, onCleanup } from "solid-js"
import { ArrowRightSquare, Copy } from "lucide-solid"
import { ArrowRightSquare, Check, Copy, Hourglass, Loader2, XCircle } from "lucide-solid"
import { stringify as stringifyYaml } from "yaml"
import { messageStoreBus } from "../stores/message-v2/bus"
import { useTheme } from "../lib/theme"
@@ -576,13 +576,13 @@ export default function ToolCall(props: ToolCallProps) {
const status = toolState()?.status || ""
switch (status) {
case "pending":
return "⏳"
return <Hourglass class="w-4 h-4" />
case "running":
return "🔄"
return <Loader2 class="w-4 h-4 animate-spin" />
case "completed":
return "✅"
return <Check class="w-4 h-4" />
case "error":
return "⚠️"
return <XCircle class="w-4 h-4" />
default:
return ""
}

View File

@@ -287,7 +287,9 @@ export const taskRenderer: ToolRenderer = {
content: promptContent()!,
cacheKey: "task:prompt",
disableScrollTracking: true,
disableHighlight: true,
// Always use the normal markdown render path for prompt (even while running)
// so the prompt doesn't visually change between running/completed states.
disableHighlight: false,
})}
</div>
</section>

View File

@@ -96,11 +96,17 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
"instanceShell.rightPanel.actions.refresh": "Refresh",
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
"instanceShell.rightPanel.sections.plan": "Plan",
"instanceShell.rightPanel.sections.plan.tooltip": "The agent's roadmap for this session. Tracks tasks, subtasks, and their completion status.",
"instanceShell.rightPanel.sections.backgroundProcesses": "Background Shells",
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Long-running processes started by the agent. You can monitor their output, stop, or terminate them.",
"instanceShell.rightPanel.sections.mcp": "MCP Servers",
"instanceShell.rightPanel.sections.mcp.tooltip": "Model Context Protocol servers that extend the agent's capabilities with external tools and services.",
"instanceShell.rightPanel.sections.lsp": "LSP Servers",
"instanceShell.rightPanel.sections.lsp.tooltip": "Language Server Protocol servers providing code intelligence, diagnostics, and language-specific features.",
"instanceShell.rightPanel.sections.plugins": "Plugins",
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins that customize the UI and server behavior, adding features beyond MCP and LSP.",
"instanceShell.sessionChanges.noSessionSelected": "Select a session to view changes.",
"instanceShell.sessionChanges.loading": "Fetching session changes...",

View File

@@ -48,7 +48,7 @@ export const instanceMessages = {
"instanceShell.commandPalette.openAriaLabel": "Abrir paleta de comandos",
"instanceShell.commandPalette.button": "Paleta de comandos",
"instanceShell.connection.ariaLabel": "Connection {status}",
"instanceShell.connection.ariaLabel": "Conexión {status}",
"instanceShell.connection.connected": "Conectada",
"instanceShell.connection.connecting": "Conectando...",
"instanceShell.connection.disconnected": "Desconectada",
@@ -93,16 +93,22 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.files": "Archivos",
"instanceShell.rightPanel.tabs.status": "Estado",
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesion",
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesión",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
"instanceShell.rightPanel.sections.plan": "Plan",
"instanceShell.rightPanel.sections.plan.tooltip": "Hoja de ruta del agente para esta sesión. Realiza el seguimiento de tareas, subtareas y su estado de finalización.",
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en segundo plano",
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Procesos de larga duración iniciados por el agente. Puedes supervisar su salida, detenerlos o terminarlos.",
"instanceShell.rightPanel.sections.mcp": "Servidores MCP",
"instanceShell.rightPanel.sections.mcp.tooltip": "Servidores del Model Context Protocol (MCP) que amplían las capacidades del agente con herramientas y servicios externos.",
"instanceShell.rightPanel.sections.lsp": "Servidores LSP",
"instanceShell.rightPanel.sections.lsp.tooltip": "Servidores del Language Server Protocol (LSP) que proporcionan inteligencia de código, diagnósticos y funciones específicas del lenguaje.",
"instanceShell.rightPanel.sections.plugins": "Plugins",
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins que personalizan el comportamiento de la UI y del servidor, y añaden funciones más allá de MCP y LSP.",
"instanceShell.sessionChanges.noSessionSelected": "Selecciona una sesion para ver los cambios.",
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesion...",
"instanceShell.sessionChanges.empty": "Aun no hay cambios.",
"instanceShell.sessionChanges.noSessionSelected": "Selecciona una sesión para ver los cambios.",
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesión...",
"instanceShell.sessionChanges.empty": "Aún no hay cambios.",
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",

View File

@@ -94,11 +94,17 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "Statut",
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
"instanceShell.rightPanel.sections.plan": "Plan",
"instanceShell.rightPanel.sections.plan.tooltip": "Feuille de route de l'agent pour cette session. Suit les tâches et leur statut d'achèvement.",
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en arrière-plan",
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Processus longs démarrés par l'agent. Vous pouvez surveiller leur sortie, les arrêter ou les terminer.",
"instanceShell.rightPanel.sections.mcp": "Serveurs MCP",
"instanceShell.rightPanel.sections.mcp.tooltip": "Serveurs du protocole Model Context Protocol qui étendent les capacités de l'agent avec des outils externes.",
"instanceShell.rightPanel.sections.lsp": "Serveurs LSP",
"instanceShell.rightPanel.sections.lsp.tooltip": "Serveurs du protocole Language Server Protocol fournissant l'intelligence de code et les diagnostics.",
"instanceShell.rightPanel.sections.plugins": "Plugins",
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins qui personnalisent le comportement de l'UI et du serveur, ajoutant des fonctionnalités au-delà de MCP et LSP.",
"instanceShell.sessionChanges.noSessionSelected": "Sélectionnez une session pour voir les changements.",
"instanceShell.sessionChanges.loading": "Récupération des changements...",

View File

@@ -94,11 +94,17 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "ステータス",
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
"instanceShell.rightPanel.sections.plan": "計画",
"instanceShell.rightPanel.sections.plan.tooltip": "このセッションにおけるエージェントのロードマップ。タスクやサブタスク、および完了状況を追跡します。",
"instanceShell.rightPanel.sections.backgroundProcesses": "バックグラウンドシェル",
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "エージェントが開始した長時間実行プロセス。出力を監視し、停止または終了できます。",
"instanceShell.rightPanel.sections.mcp": "MCP サーバー",
"instanceShell.rightPanel.sections.mcp.tooltip": "Model Context Protocol (MCP) サーバー。外部ツールやサービスでエージェントの機能を拡張します。",
"instanceShell.rightPanel.sections.lsp": "LSP サーバー",
"instanceShell.rightPanel.sections.lsp.tooltip": "Language Server Protocolサーバーがコードインテリジェンス、診断、言語固有の機能を提供します。",
"instanceShell.rightPanel.sections.plugins": "プラグイン",
"instanceShell.rightPanel.sections.plugins.tooltip": "UI とサーバーの動作をカスタマイズし、MCP や LSP 以外の機能も追加できるプラグイン。",
"instanceShell.sessionChanges.noSessionSelected": "変更を表示するにはセッションを選択してください。",
"instanceShell.sessionChanges.loading": "変更を取得中...",

View File

@@ -94,11 +94,17 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "Статус",
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
"instanceShell.rightPanel.sections.plan": "План",
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые Shell",
"instanceShell.rightPanel.sections.plan.tooltip": "Дорожная карта агента для этой сессии. Отслеживает задачи и их статус выполнения.",
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые оболочки",
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Долгоработающие процессы, запущенные агентом. Вы можете следить за их выводом, останавливать или завершать их.",
"instanceShell.rightPanel.sections.mcp": "MCP-серверы",
"instanceShell.rightPanel.sections.mcp.tooltip": "Серверы протокола Model Context Protocol, расширяющие возможности агента внешними инструментами.",
"instanceShell.rightPanel.sections.lsp": "LSP-серверы",
"instanceShell.rightPanel.sections.lsp.tooltip": "Серверы протокола Language Server Protocol, обеспечивающие интеллектуальную поддержку кода и диагностику.",
"instanceShell.rightPanel.sections.plugins": "Плагины",
"instanceShell.rightPanel.sections.plugins.tooltip": "Плагины, настраивающие поведение интерфейса и сервера, добавляющие функции поверх MCP и LSP.",
"instanceShell.sessionChanges.noSessionSelected": "Выберите сессию, чтобы просмотреть изменения.",
"instanceShell.sessionChanges.loading": "Загрузка изменений...",
@@ -128,7 +134,7 @@ export const instanceMessages = {
"versionPill.uiWithVersion": "UI {version}",
"versionPill.source": " ({source})",
"opencodeBinarySelector.title": "OpenCode Binary",
"opencodeBinarySelector.title": "Бинарник OpenCode",
"opencodeBinarySelector.subtitle": "Выберите, какой исполняемый файл OpenCode запускать",
"opencodeBinarySelector.customPath.placeholder": "Введите путь к бинарнику opencode…",
"opencodeBinarySelector.actions.add": "Добавить",

View File

@@ -94,11 +94,17 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "状态",
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
"instanceShell.rightPanel.sections.plan": "计划",
"instanceShell.rightPanel.sections.plan.tooltip": "代理的路线图。跟踪任务、子任务及其完成状态。",
"instanceShell.rightPanel.sections.backgroundProcesses": "后台 Shell",
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "代理启动的后台进程。您可以监控其输出、停止或终止它们。",
"instanceShell.rightPanel.sections.mcp": "MCP 服务器",
"instanceShell.rightPanel.sections.mcp.tooltip": "模型上下文协议服务器,使用外部工具和服务扩展代理能力。",
"instanceShell.rightPanel.sections.lsp": "LSP 服务器",
"instanceShell.rightPanel.sections.lsp.tooltip": "语言服务器协议服务器,提供代码智能、诊断和语言特定的功能。",
"instanceShell.rightPanel.sections.plugins": "插件",
"instanceShell.rightPanel.sections.plugins.tooltip": "自定义 UI 和服务器行为的插件,添加超出 MCP 和 LSP 的功能。",
"instanceShell.sessionChanges.noSessionSelected": "选择会话以查看更改。",
"instanceShell.sessionChanges.loading": "正在获取会话更改...",

View File

@@ -127,17 +127,23 @@ async function ensureLanguages(content: string) {
if (highlightSuppressed) {
return
}
// Parse code fences to extract language tokens
// Updated regex to capture optional language tokens and handle trailing annotations
const codeBlockRegex = /```[ \t]*([A-Za-z0-9_.+#-]+)?[^`]*?```/g
const foundLanguages = new Set<string>()
let match
while ((match = codeBlockRegex.exec(content)) !== null) {
const langToken = match[1]
if (langToken && langToken.trim()) {
foundLanguages.add(langToken.trim())
}
// Extract code-fence language tokens via `marked` so we correctly handle code blocks
// that contain backticks (e.g. JS template literals). Regex-based fence scans tend
// to miss these and prevent languages from loading.
const foundLanguages = new Set<string>()
try {
const tokens = marked.lexer(content) as any
marked.walkTokens(tokens, (token: any) => {
if (token?.type !== "code") return
const langToken = typeof token.lang === "string" ? token.lang : ""
if (langToken.trim()) {
foundLanguages.add(langToken.trim())
}
})
} catch {
// If tokenization fails for any reason, skip language preloading.
return
}
// Queue language loading tasks

View File

@@ -40,7 +40,7 @@ import {
} from "./instances"
import { showAlertDialog } from "./alerts"
import { createClientSession, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
import { sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
import { ensureSessionParentExpanded, sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
import { normalizeMessagePart } from "./message-v2/normalizers"
import { updateSessionInfo } from "./message-v2/session-info"
import { tGlobal } from "../lib/i18n"
@@ -108,6 +108,8 @@ interface TuiToastEvent {
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
let parentToExpand: string | null = null
withSession(instanceId, sessionId, (session) => {
const current = session.status ?? "idle"
if (current === status) return false
@@ -117,7 +119,17 @@ function applySessionStatus(instanceId: string, sessionId: string, status: Sessi
}
session.status = status
// Auto-expand the parent thread when a child session starts working.
// Users can still collapse it; we only expand on the transition.
if (session.parentId && status === "working" && current !== "working") {
parentToExpand = session.parentId
}
})
if (parentToExpand) {
ensureSessionParentExpanded(instanceId, parentToExpand)
}
}
async function fetchSessionInfo(instanceId: string, sessionId: string, directory?: string): Promise<Session | null> {
@@ -158,6 +170,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
let updatedInstanceSessions: Map<string, Session> | undefined
let shouldExpandParent: string | null = null
setSessions((prev) => {
const next = new Map(prev)
@@ -174,11 +187,19 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
instanceSessions.set(sessionId, merged)
next.set(instanceId, instanceSessions)
updatedInstanceSessions = instanceSessions
if (merged.parentId && merged.status === "working" && (existing?.status ?? "idle") !== "working") {
shouldExpandParent = merged.parentId
}
return next
})
syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)
if (shouldExpandParent) {
ensureSessionParentExpanded(instanceId, shouldExpandParent)
}
return fetched
} catch (error) {
log.error("Failed to fetch session info", error)

View File

@@ -347,10 +347,23 @@ function clearActiveParentSession(instanceId: string): void {
}
function setSessionStatus(instanceId: string, sessionId: string, status: SessionStatus): void {
let parentToExpand: string | null = null
withSession(instanceId, sessionId, (session) => {
if (session.status === status) return false
const previous = session.status
session.status = status
// If a child session starts working, auto-expand its parent thread once.
// Users can still collapse it afterwards; we only expand on the transition.
if (session.parentId && status === "working" && previous !== "working") {
parentToExpand = session.parentId
}
})
if (parentToExpand) {
ensureSessionParentExpanded(instanceId, parentToExpand)
}
}
function getActiveParentSession(instanceId: string): Session | null {

View File

@@ -130,6 +130,19 @@
color: var(--text-primary);
}
/* Make the command palette trigger stand out in the header. */
.connection-status-button.command-palette-button {
border-radius: 0;
@apply text-sm px-2 py-1 border border-base transition-colors;
background-color: var(--surface-base);
color: var(--text-primary);
}
.connection-status-button.command-palette-button:hover {
background-color: var(--surface-hover);
color: var(--text-primary);
}
.connection-status-button:hover {
background-color: var(--surface-hover);
}

View File

@@ -412,7 +412,7 @@
}
.right-panel-accordion-trigger {
@apply w-full flex items-center justify-between gap-3 px-3 py-2.5 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-150;
@apply w-full flex items-center justify-between px-3 py-2.5 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-150;
color: var(--text-secondary);
background-color: transparent;
}
@@ -422,6 +422,11 @@
color: var(--text-primary);
}
.section-left {
@apply flex items-center;
flex-shrink: 0;
}
.right-panel-accordion-chevron {
@apply h-4 w-4 transition-transform duration-200;
color: var(--text-muted);
@@ -441,6 +446,51 @@
min-height: 0;
}
/* Section info tooltip */
.section-info-trigger {
@apply inline-flex items-center justify-center p-0.5 rounded transition-all duration-150;
color: var(--text-muted);
flex-shrink: 0;
}
.section-info-trigger:hover {
color: var(--text-primary);
background-color: var(--surface-hover);
}
.section-label {
margin-left: 2px;
}
.section-info-icon {
@apply w-3.5 h-3.5;
}
.section-info-tooltip {
@apply max-w-xs px-3 py-2 text-xs rounded-lg border shadow-lg;
background-color: var(--surface-base);
border-color: var(--border-base);
color: var(--text-primary);
animation: tooltipShow 150ms ease-out;
transform-origin: var(--kb-tooltip-content-transform-origin);
z-index: 9999;
}
.section-info-tooltip[data-expanded] {
animation: tooltipShow 150ms ease-out;
}
@keyframes tooltipShow {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Background process cards in status panel */
.status-process-card {
@apply rounded-lg border flex flex-col gap-2 p-3 transition-all duration-150;