From decba25a08336af8d487ff9a66e59a42a02f4f31 Mon Sep 17 00:00:00 2001
From: ssdfasd <2156608475@qq.com>
Date: Thu, 12 Mar 2026 21:33:50 +0800
Subject: [PATCH] Initial commit: restructure to flat layout with ui/ and web/
at root
---
.gitignore | 54 +
.nvmrc | 1 +
AGENTS.md | 128 +
CHANGELOG.md | 776 +
CONTRIBUTING.md | 92 +
LICENSE | 21 +
SECURITY.md | 32 +
bun.lock | 2346 +++
components.json | 21 +
eslint.config.js | 23 +
openchamber.sed | 19 +
package.json | 167 +
postcss.config.js | 5 +
run.bat | 23 +
tsconfig.json | 19 +
ui/package.json | 113 +
ui/src/App.css | 42 +
ui/src/App.tsx | 548 +
ui/src/assets/icons/file-types/3d.svg | 1 +
ui/src/assets/icons/file-types/README.md | 26 +
ui/src/assets/icons/file-types/abap.svg | 1 +
ui/src/assets/icons/file-types/abc.svg | 1 +
.../assets/icons/file-types/actionscript.svg | 1 +
ui/src/assets/icons/file-types/ada.svg | 1 +
.../icons/file-types/adobe-illustrator.svg | 1 +
.../file-types/adobe-illustrator_light.svg | 1 +
.../icons/file-types/adobe-photoshop.svg | 1 +
.../file-types/adobe-photoshop_light.svg | 1 +
ui/src/assets/icons/file-types/adobe-swc.svg | 1 +
ui/src/assets/icons/file-types/adonis.svg | 1 +
ui/src/assets/icons/file-types/advpl.svg | 1 +
ui/src/assets/icons/file-types/amplify.svg | 1 +
ui/src/assets/icons/file-types/android.svg | 1 +
ui/src/assets/icons/file-types/angular.svg | 1 +
ui/src/assets/icons/file-types/antlr.svg | 1 +
.../assets/icons/file-types/apiblueprint.svg | 1 +
ui/src/assets/icons/file-types/apollo.svg | 1 +
.../assets/icons/file-types/applescript.svg | 1 +
.../assets/icons/file-types/apps-script.svg | 1 +
ui/src/assets/icons/file-types/appveyor.svg | 1 +
.../assets/icons/file-types/architecture.svg | 1 +
ui/src/assets/icons/file-types/arduino.svg | 1 +
ui/src/assets/icons/file-types/asciidoc.svg | 1 +
ui/src/assets/icons/file-types/assembly.svg | 1 +
.../assets/icons/file-types/astro-config.svg | 1 +
ui/src/assets/icons/file-types/astro.svg | 1 +
ui/src/assets/icons/file-types/astyle.svg | 1 +
ui/src/assets/icons/file-types/audio.svg | 1 +
ui/src/assets/icons/file-types/aurelia.svg | 1 +
ui/src/assets/icons/file-types/authors.svg | 1 +
ui/src/assets/icons/file-types/auto.svg | 1 +
ui/src/assets/icons/file-types/auto_light.svg | 12 +
ui/src/assets/icons/file-types/autohotkey.svg | 1 +
ui/src/assets/icons/file-types/autoit.svg | 1 +
.../icons/file-types/azure-pipelines.svg | 1 +
ui/src/assets/icons/file-types/azure.svg | 1 +
ui/src/assets/icons/file-types/babel.svg | 1 +
ui/src/assets/icons/file-types/ballerina.svg | 1 +
ui/src/assets/icons/file-types/bazel.svg | 1 +
ui/src/assets/icons/file-types/bbx.svg | 1 +
ui/src/assets/icons/file-types/beancount.svg | 1 +
ui/src/assets/icons/file-types/bench-js.svg | 1 +
ui/src/assets/icons/file-types/bench-jsx.svg | 1 +
ui/src/assets/icons/file-types/bench-ts.svg | 1 +
.../assets/icons/file-types/bibliography.svg | 1 +
.../assets/icons/file-types/bibtex-style.svg | 1 +
ui/src/assets/icons/file-types/bicep.svg | 1 +
ui/src/assets/icons/file-types/biome.svg | 1 +
ui/src/assets/icons/file-types/bitbucket.svg | 1 +
ui/src/assets/icons/file-types/bithound.svg | 1 +
ui/src/assets/icons/file-types/blender.svg | 1 +
ui/src/assets/icons/file-types/blink.svg | 1 +
.../assets/icons/file-types/blink_light.svg | 1 +
ui/src/assets/icons/file-types/blitz.svg | 1 +
ui/src/assets/icons/file-types/bower.svg | 1 +
ui/src/assets/icons/file-types/brainfuck.svg | 1 +
.../assets/icons/file-types/browserlist.svg | 1 +
.../icons/file-types/browserlist_light.svg | 1 +
ui/src/assets/icons/file-types/bruno.svg | 1 +
ui/src/assets/icons/file-types/buck.svg | 1 +
.../assets/icons/file-types/bucklescript.svg | 1 +
ui/src/assets/icons/file-types/buildkite.svg | 1 +
ui/src/assets/icons/file-types/bun.svg | 1 +
ui/src/assets/icons/file-types/bun_light.svg | 1 +
ui/src/assets/icons/file-types/c.svg | 1 +
ui/src/assets/icons/file-types/c3.svg | 1 +
ui/src/assets/icons/file-types/cabal.svg | 1 +
ui/src/assets/icons/file-types/caddy.svg | 1 +
ui/src/assets/icons/file-types/cadence.svg | 1 +
ui/src/assets/icons/file-types/cairo.svg | 1 +
ui/src/assets/icons/file-types/cake.svg | 1 +
ui/src/assets/icons/file-types/capacitor.svg | 1 +
ui/src/assets/icons/file-types/capnp.svg | 1 +
ui/src/assets/icons/file-types/cbx.svg | 1 +
ui/src/assets/icons/file-types/cds.svg | 1 +
.../assets/icons/file-types/certificate.svg | 1 +
ui/src/assets/icons/file-types/changelog.svg | 1 +
ui/src/assets/icons/file-types/chess.svg | 1 +
.../assets/icons/file-types/chess_light.svg | 1 +
ui/src/assets/icons/file-types/chrome.svg | 1 +
ui/src/assets/icons/file-types/circleci.svg | 1 +
.../icons/file-types/circleci_light.svg | 1 +
ui/src/assets/icons/file-types/citation.svg | 1 +
ui/src/assets/icons/file-types/clangd.svg | 1 +
ui/src/assets/icons/file-types/claude.svg | 1 +
ui/src/assets/icons/file-types/cline.svg | 1 +
ui/src/assets/icons/file-types/clojure.svg | 1 +
.../assets/icons/file-types/cloudfoundry.svg | 1 +
ui/src/assets/icons/file-types/cmake.svg | 1 +
ui/src/assets/icons/file-types/coala.svg | 1 +
ui/src/assets/icons/file-types/cobol.svg | 1 +
ui/src/assets/icons/file-types/coconut.svg | 1 +
.../assets/icons/file-types/code-climate.svg | 1 +
.../icons/file-types/code-climate_light.svg | 1 +
ui/src/assets/icons/file-types/codecov.svg | 1 +
ui/src/assets/icons/file-types/codeowners.svg | 1 +
.../assets/icons/file-types/coderabbit-ai.svg | 1 +
ui/src/assets/icons/file-types/coffee.svg | 1 +
ui/src/assets/icons/file-types/coldfusion.svg | 1 +
.../icons/file-types/coloredpetrinets.svg | 1 +
ui/src/assets/icons/file-types/command.svg | 1 +
ui/src/assets/icons/file-types/commitizen.svg | 1 +
ui/src/assets/icons/file-types/commitlint.svg | 1 +
ui/src/assets/icons/file-types/concourse.svg | 1 +
ui/src/assets/icons/file-types/conduct.svg | 1 +
ui/src/assets/icons/file-types/console.svg | 1 +
.../assets/icons/file-types/contentlayer.svg | 1 +
ui/src/assets/icons/file-types/context.svg | 1 +
.../assets/icons/file-types/contributing.svg | 1 +
ui/src/assets/icons/file-types/controller.svg | 1 +
ui/src/assets/icons/file-types/copilot.svg | 1 +
.../assets/icons/file-types/copilot_light.svg | 1 +
ui/src/assets/icons/file-types/cpp.svg | 1 +
ui/src/assets/icons/file-types/craco.svg | 1 +
ui/src/assets/icons/file-types/credits.svg | 1 +
ui/src/assets/icons/file-types/crystal.svg | 1 +
.../assets/icons/file-types/crystal_light.svg | 1 +
ui/src/assets/icons/file-types/csharp.svg | 1 +
ui/src/assets/icons/file-types/css-map.svg | 1 +
ui/src/assets/icons/file-types/css.svg | 1 +
ui/src/assets/icons/file-types/cucumber.svg | 1 +
ui/src/assets/icons/file-types/cuda.svg | 1 +
ui/src/assets/icons/file-types/cursor.svg | 1 +
.../assets/icons/file-types/cursor_light.svg | 1 +
ui/src/assets/icons/file-types/cypress.svg | 1 +
ui/src/assets/icons/file-types/d.svg | 1 +
ui/src/assets/icons/file-types/dart.svg | 1 +
.../icons/file-types/dart_generated.svg | 1 +
ui/src/assets/icons/file-types/database.svg | 1 +
ui/src/assets/icons/file-types/deepsource.svg | 1 +
.../assets/icons/file-types/denizenscript.svg | 1 +
ui/src/assets/icons/file-types/deno.svg | 1 +
ui/src/assets/icons/file-types/deno_light.svg | 1 +
ui/src/assets/icons/file-types/dependabot.svg | 1 +
.../icons/file-types/dependencies-update.svg | 1 +
ui/src/assets/icons/file-types/dhall.svg | 1 +
ui/src/assets/icons/file-types/diff.svg | 1 +
ui/src/assets/icons/file-types/dinophp.svg | 1 +
ui/src/assets/icons/file-types/disc.svg | 1 +
ui/src/assets/icons/file-types/django.svg | 1 +
ui/src/assets/icons/file-types/dll.svg | 1 +
ui/src/assets/icons/file-types/docker.svg | 1 +
.../icons/file-types/doctex-installer.svg | 1 +
ui/src/assets/icons/file-types/document.svg | 1 +
ui/src/assets/icons/file-types/dotjs.svg | 1 +
ui/src/assets/icons/file-types/drawio.svg | 1 +
ui/src/assets/icons/file-types/drizzle.svg | 1 +
ui/src/assets/icons/file-types/drone.svg | 1 +
.../assets/icons/file-types/drone_light.svg | 4 +
ui/src/assets/icons/file-types/duc.svg | 1 +
ui/src/assets/icons/file-types/dune.svg | 1 +
ui/src/assets/icons/file-types/edge.svg | 1 +
.../assets/icons/file-types/editorconfig.svg | 1 +
ui/src/assets/icons/file-types/ejs.svg | 1 +
ui/src/assets/icons/file-types/elixir.svg | 1 +
ui/src/assets/icons/file-types/elm.svg | 1 +
ui/src/assets/icons/file-types/email.svg | 1 +
ui/src/assets/icons/file-types/ember.svg | 1 +
ui/src/assets/icons/file-types/epub.svg | 1 +
ui/src/assets/icons/file-types/erlang.svg | 1 +
ui/src/assets/icons/file-types/esbuild.svg | 1 +
ui/src/assets/icons/file-types/eslint.svg | 1 +
ui/src/assets/icons/file-types/excalidraw.svg | 1 +
ui/src/assets/icons/file-types/exe.svg | 1 +
ui/src/assets/icons/file-types/fastlane.svg | 1 +
ui/src/assets/icons/file-types/favicon.svg | 1 +
ui/src/assets/icons/file-types/figma.svg | 1 +
ui/src/assets/icons/file-types/firebase.svg | 1 +
ui/src/assets/icons/file-types/flash.svg | 1 +
ui/src/assets/icons/file-types/flow.svg | 1 +
.../icons/file-types/folder-admin-open.svg | 6 +
.../assets/icons/file-types/folder-admin.svg | 6 +
.../icons/file-types/folder-android-open.svg | 1 +
.../icons/file-types/folder-android.svg | 1 +
.../icons/file-types/folder-angular-open.svg | 1 +
.../icons/file-types/folder-angular.svg | 1 +
.../file-types/folder-animation-open.svg | 1 +
.../icons/file-types/folder-animation.svg | 1 +
.../icons/file-types/folder-ansible-open.svg | 1 +
.../icons/file-types/folder-ansible.svg | 1 +
.../icons/file-types/folder-api-open.svg | 1 +
ui/src/assets/icons/file-types/folder-api.svg | 1 +
.../icons/file-types/folder-apollo-open.svg | 1 +
.../assets/icons/file-types/folder-apollo.svg | 1 +
.../icons/file-types/folder-app-open.svg | 1 +
ui/src/assets/icons/file-types/folder-app.svg | 1 +
.../icons/file-types/folder-archive-open.svg | 1 +
.../icons/file-types/folder-archive.svg | 1 +
.../icons/file-types/folder-astro-open.svg | 1 +
.../assets/icons/file-types/folder-astro.svg | 1 +
.../icons/file-types/folder-atom-open.svg | 1 +
.../assets/icons/file-types/folder-atom.svg | 1 +
.../file-types/folder-attachment-open.svg | 1 +
.../icons/file-types/folder-attachment.svg | 1 +
.../icons/file-types/folder-audio-open.svg | 1 +
.../assets/icons/file-types/folder-audio.svg | 1 +
.../icons/file-types/folder-aurelia-open.svg | 1 +
.../icons/file-types/folder-aurelia.svg | 1 +
.../icons/file-types/folder-aws-open.svg | 1 +
ui/src/assets/icons/file-types/folder-aws.svg | 1 +
.../folder-azure-pipelines-open.svg | 1 +
.../file-types/folder-azure-pipelines.svg | 1 +
.../icons/file-types/folder-backup-open.svg | 1 +
.../assets/icons/file-types/folder-backup.svg | 1 +
.../icons/file-types/folder-base-open.svg | 1 +
.../assets/icons/file-types/folder-base.svg | 1 +
.../icons/file-types/folder-batch-open.svg | 1 +
.../assets/icons/file-types/folder-batch.svg | 1 +
.../file-types/folder-benchmark-open.svg | 1 +
.../icons/file-types/folder-benchmark.svg | 1 +
.../file-types/folder-bibliography-open.svg | 1 +
.../icons/file-types/folder-bibliography.svg | 1 +
.../icons/file-types/folder-bicep-open.svg | 1 +
.../assets/icons/file-types/folder-bicep.svg | 1 +
.../icons/file-types/folder-blender-open.svg | 1 +
.../icons/file-types/folder-blender.svg | 1 +
.../icons/file-types/folder-bloc-open.svg | 1 +
.../assets/icons/file-types/folder-bloc.svg | 1 +
.../icons/file-types/folder-bower-open.svg | 1 +
.../assets/icons/file-types/folder-bower.svg | 1 +
.../file-types/folder-buildkite-open.svg | 1 +
.../icons/file-types/folder-buildkite.svg | 1 +
.../icons/file-types/folder-cart-open.svg | 1 +
.../assets/icons/file-types/folder-cart.svg | 1 +
.../file-types/folder-changesets-open.svg | 1 +
.../icons/file-types/folder-changesets.svg | 1 +
.../icons/file-types/folder-ci-open.svg | 1 +
ui/src/assets/icons/file-types/folder-ci.svg | 1 +
.../icons/file-types/folder-circleci-open.svg | 7 +
.../icons/file-types/folder-circleci.svg | 7 +
.../icons/file-types/folder-class-open.svg | 1 +
.../assets/icons/file-types/folder-class.svg | 1 +
.../icons/file-types/folder-claude-open.svg | 1 +
.../assets/icons/file-types/folder-claude.svg | 1 +
.../icons/file-types/folder-client-open.svg | 1 +
.../assets/icons/file-types/folder-client.svg | 1 +
.../icons/file-types/folder-cline-open.svg | 1 +
.../assets/icons/file-types/folder-cline.svg | 1 +
.../folder-cloud-functions-open.svg | 1 +
.../file-types/folder-cloud-functions.svg | 1 +
.../file-types/folder-cloudflare-open.svg | 1 +
.../icons/file-types/folder-cloudflare.svg | 1 +
.../icons/file-types/folder-cluster-open.svg | 1 +
.../icons/file-types/folder-cluster.svg | 1 +
.../icons/file-types/folder-cobol-open.svg | 1 +
.../assets/icons/file-types/folder-cobol.svg | 1 +
.../icons/file-types/folder-command-open.svg | 1 +
.../icons/file-types/folder-command.svg | 1 +
.../file-types/folder-components-open.svg | 1 +
.../icons/file-types/folder-components.svg | 1 +
.../icons/file-types/folder-config-open.svg | 1 +
.../assets/icons/file-types/folder-config.svg | 1 +
.../file-types/folder-connection-open.svg | 1 +
.../icons/file-types/folder-connection.svg | 1 +
.../icons/file-types/folder-console-open.svg | 1 +
.../icons/file-types/folder-console.svg | 1 +
.../icons/file-types/folder-constant-open.svg | 1 +
.../icons/file-types/folder-constant.svg | 1 +
.../file-types/folder-container-open.svg | 1 +
.../icons/file-types/folder-container.svg | 1 +
.../icons/file-types/folder-content-open.svg | 1 +
.../icons/file-types/folder-content.svg | 1 +
.../icons/file-types/folder-context-open.svg | 1 +
.../icons/file-types/folder-context.svg | 1 +
.../icons/file-types/folder-contract-open.svg | 1 +
.../icons/file-types/folder-contract.svg | 1 +
.../file-types/folder-controller-open.svg | 1 +
.../icons/file-types/folder-controller.svg | 1 +
.../icons/file-types/folder-core-open.svg | 1 +
.../assets/icons/file-types/folder-core.svg | 1 +
.../icons/file-types/folder-coverage-open.svg | 1 +
.../icons/file-types/folder-coverage.svg | 1 +
.../icons/file-types/folder-css-open.svg | 1 +
ui/src/assets/icons/file-types/folder-css.svg | 1 +
.../icons/file-types/folder-cursor-open.svg | 1 +
.../file-types/folder-cursor-open_light.svg | 1 +
.../assets/icons/file-types/folder-cursor.svg | 1 +
.../icons/file-types/folder-cursor_light.svg | 1 +
.../icons/file-types/folder-custom-open.svg | 1 +
.../assets/icons/file-types/folder-custom.svg | 1 +
.../icons/file-types/folder-cypress-open.svg | 1 +
.../icons/file-types/folder-cypress.svg | 1 +
.../icons/file-types/folder-dart-open.svg | 1 +
.../assets/icons/file-types/folder-dart.svg | 1 +
.../icons/file-types/folder-database-open.svg | 1 +
.../icons/file-types/folder-database.svg | 1 +
.../icons/file-types/folder-debug-open.svg | 1 +
.../assets/icons/file-types/folder-debug.svg | 1 +
.../file-types/folder-decorators-open.svg | 1 +
.../icons/file-types/folder-decorators.svg | 1 +
.../icons/file-types/folder-delta-open.svg | 1 +
.../assets/icons/file-types/folder-delta.svg | 1 +
.../icons/file-types/folder-desktop-open.svg | 1 +
.../icons/file-types/folder-desktop.svg | 1 +
.../file-types/folder-directive-open.svg | 1 +
.../icons/file-types/folder-directive.svg | 1 +
.../icons/file-types/folder-dist-open.svg | 1 +
.../assets/icons/file-types/folder-dist.svg | 1 +
.../icons/file-types/folder-docker-open.svg | 1 +
.../assets/icons/file-types/folder-docker.svg | 1 +
.../icons/file-types/folder-docs-open.svg | 1 +
.../assets/icons/file-types/folder-docs.svg | 1 +
.../icons/file-types/folder-download-open.svg | 1 +
.../icons/file-types/folder-download.svg | 1 +
.../icons/file-types/folder-drizzle-open.svg | 1 +
.../icons/file-types/folder-drizzle.svg | 1 +
.../icons/file-types/folder-dump-open.svg | 1 +
.../assets/icons/file-types/folder-dump.svg | 1 +
.../icons/file-types/folder-element-open.svg | 1 +
.../icons/file-types/folder-element.svg | 1 +
.../icons/file-types/folder-enum-open.svg | 1 +
.../assets/icons/file-types/folder-enum.svg | 1 +
.../file-types/folder-environment-open.svg | 1 +
.../icons/file-types/folder-environment.svg | 1 +
.../icons/file-types/folder-error-open.svg | 1 +
.../assets/icons/file-types/folder-error.svg | 1 +
.../icons/file-types/folder-event-open.svg | 1 +
.../assets/icons/file-types/folder-event.svg | 1 +
.../icons/file-types/folder-examples-open.svg | 1 +
.../icons/file-types/folder-examples.svg | 1 +
.../icons/file-types/folder-expo-open.svg | 1 +
.../assets/icons/file-types/folder-expo.svg | 1 +
.../icons/file-types/folder-export-open.svg | 1 +
.../assets/icons/file-types/folder-export.svg | 1 +
.../icons/file-types/folder-fastlane-open.svg | 1 +
.../icons/file-types/folder-fastlane.svg | 1 +
.../icons/file-types/folder-favicon-open.svg | 1 +
.../icons/file-types/folder-favicon.svg | 1 +
.../icons/file-types/folder-firebase-open.svg | 1 +
.../icons/file-types/folder-firebase.svg | 1 +
.../file-types/folder-firestore-open.svg | 1 +
.../icons/file-types/folder-firestore.svg | 1 +
.../icons/file-types/folder-flow-open.svg | 6 +
.../assets/icons/file-types/folder-flow.svg | 6 +
.../icons/file-types/folder-flutter-open.svg | 1 +
.../icons/file-types/folder-flutter.svg | 1 +
.../icons/file-types/folder-font-open.svg | 1 +
.../assets/icons/file-types/folder-font.svg | 1 +
.../icons/file-types/folder-forgejo-open.svg | 1 +
.../icons/file-types/folder-forgejo.svg | 1 +
.../file-types/folder-functions-open.svg | 1 +
.../icons/file-types/folder-functions.svg | 1 +
.../file-types/folder-gamemaker-open.svg | 1 +
.../icons/file-types/folder-gamemaker.svg | 1 +
.../file-types/folder-generator-open.svg | 1 +
.../icons/file-types/folder-generator.svg | 1 +
.../file-types/folder-gh-workflows-open.svg | 6 +
.../icons/file-types/folder-gh-workflows.svg | 6 +
.../icons/file-types/folder-git-open.svg | 1 +
ui/src/assets/icons/file-types/folder-git.svg | 1 +
.../icons/file-types/folder-gitea-open.svg | 1 +
.../assets/icons/file-types/folder-gitea.svg | 1 +
.../icons/file-types/folder-github-open.svg | 6 +
.../assets/icons/file-types/folder-github.svg | 6 +
.../icons/file-types/folder-gitlab-open.svg | 1 +
.../assets/icons/file-types/folder-gitlab.svg | 1 +
.../icons/file-types/folder-global-open.svg | 1 +
.../assets/icons/file-types/folder-global.svg | 1 +
.../icons/file-types/folder-godot-open.svg | 1 +
.../assets/icons/file-types/folder-godot.svg | 1 +
.../icons/file-types/folder-gradle-open.svg | 1 +
.../assets/icons/file-types/folder-gradle.svg | 1 +
.../icons/file-types/folder-graphql-open.svg | 1 +
.../icons/file-types/folder-graphql.svg | 1 +
.../icons/file-types/folder-guard-open.svg | 1 +
.../assets/icons/file-types/folder-guard.svg | 1 +
.../icons/file-types/folder-gulp-open.svg | 1 +
.../assets/icons/file-types/folder-gulp.svg | 1 +
.../icons/file-types/folder-helm-open.svg | 1 +
.../assets/icons/file-types/folder-helm.svg | 1 +
.../icons/file-types/folder-helper-open.svg | 1 +
.../assets/icons/file-types/folder-helper.svg | 1 +
.../icons/file-types/folder-home-open.svg | 1 +
.../assets/icons/file-types/folder-home.svg | 1 +
.../icons/file-types/folder-hook-open.svg | 1 +
.../assets/icons/file-types/folder-hook.svg | 1 +
.../icons/file-types/folder-husky-open.svg | 1 +
.../assets/icons/file-types/folder-husky.svg | 1 +
.../icons/file-types/folder-i18n-open.svg | 1 +
.../assets/icons/file-types/folder-i18n.svg | 1 +
.../icons/file-types/folder-images-open.svg | 1 +
.../assets/icons/file-types/folder-images.svg | 1 +
.../icons/file-types/folder-import-open.svg | 1 +
.../assets/icons/file-types/folder-import.svg | 1 +
.../icons/file-types/folder-include-open.svg | 1 +
.../icons/file-types/folder-include.svg | 1 +
.../icons/file-types/folder-intellij-open.svg | 39 +
.../file-types/folder-intellij-open_light.svg | 1 +
.../icons/file-types/folder-intellij.svg | 39 +
.../file-types/folder-intellij_light.svg | 1 +
.../file-types/folder-interceptor-open.svg | 1 +
.../icons/file-types/folder-interceptor.svg | 1 +
.../file-types/folder-interface-open.svg | 1 +
.../icons/file-types/folder-interface.svg | 1 +
.../icons/file-types/folder-ios-open.svg | 1 +
ui/src/assets/icons/file-types/folder-ios.svg | 1 +
.../icons/file-types/folder-java-open.svg | 1 +
.../assets/icons/file-types/folder-java.svg | 1 +
.../file-types/folder-javascript-open.svg | 1 +
.../icons/file-types/folder-javascript.svg | 1 +
.../icons/file-types/folder-jinja-open.svg | 1 +
.../file-types/folder-jinja-open_light.svg | 1 +
.../assets/icons/file-types/folder-jinja.svg | 1 +
.../icons/file-types/folder-jinja_light.svg | 1 +
.../icons/file-types/folder-job-open.svg | 1 +
ui/src/assets/icons/file-types/folder-job.svg | 1 +
.../icons/file-types/folder-json-open.svg | 1 +
.../assets/icons/file-types/folder-json.svg | 1 +
.../icons/file-types/folder-jupyter-open.svg | 1 +
.../icons/file-types/folder-jupyter.svg | 1 +
.../icons/file-types/folder-keys-open.svg | 1 +
.../assets/icons/file-types/folder-keys.svg | 1 +
.../file-types/folder-kubernetes-open.svg | 1 +
.../icons/file-types/folder-kubernetes.svg | 1 +
.../icons/file-types/folder-kusto-open.svg | 1 +
.../assets/icons/file-types/folder-kusto.svg | 1 +
.../icons/file-types/folder-layout-open.svg | 1 +
.../assets/icons/file-types/folder-layout.svg | 1 +
.../icons/file-types/folder-lefthook-open.svg | 1 +
.../icons/file-types/folder-lefthook.svg | 1 +
.../icons/file-types/folder-less-open.svg | 1 +
.../assets/icons/file-types/folder-less.svg | 1 +
.../icons/file-types/folder-lib-open.svg | 1 +
ui/src/assets/icons/file-types/folder-lib.svg | 1 +
.../icons/file-types/folder-link-open.svg | 1 +
.../assets/icons/file-types/folder-link.svg | 1 +
.../icons/file-types/folder-linux-open.svg | 1 +
.../assets/icons/file-types/folder-linux.svg | 1 +
.../file-types/folder-liquibase-open.svg | 1 +
.../icons/file-types/folder-liquibase.svg | 1 +
.../icons/file-types/folder-log-open.svg | 1 +
ui/src/assets/icons/file-types/folder-log.svg | 1 +
.../icons/file-types/folder-lottie-open.svg | 1 +
.../assets/icons/file-types/folder-lottie.svg | 1 +
.../icons/file-types/folder-lua-open.svg | 1 +
ui/src/assets/icons/file-types/folder-lua.svg | 1 +
.../icons/file-types/folder-luau-open.svg | 1 +
.../assets/icons/file-types/folder-luau.svg | 1 +
.../icons/file-types/folder-macos-open.svg | 6 +
.../assets/icons/file-types/folder-macos.svg | 6 +
.../icons/file-types/folder-mail-open.svg | 1 +
.../assets/icons/file-types/folder-mail.svg | 1 +
.../icons/file-types/folder-mappings-open.svg | 1 +
.../icons/file-types/folder-mappings.svg | 1 +
.../icons/file-types/folder-markdown-open.svg | 1 +
.../icons/file-types/folder-markdown.svg | 1 +
.../file-types/folder-mercurial-open.svg | 1 +
.../icons/file-types/folder-mercurial.svg | 1 +
.../icons/file-types/folder-messages-open.svg | 1 +
.../icons/file-types/folder-messages.svg | 1 +
.../icons/file-types/folder-meta-open.svg | 1 +
.../assets/icons/file-types/folder-meta.svg | 1 +
.../file-types/folder-middleware-open.svg | 1 +
.../icons/file-types/folder-middleware.svg | 1 +
.../icons/file-types/folder-mjml-open.svg | 1 +
.../assets/icons/file-types/folder-mjml.svg | 1 +
.../icons/file-types/folder-mobile-open.svg | 1 +
.../assets/icons/file-types/folder-mobile.svg | 1 +
.../icons/file-types/folder-mock-open.svg | 1 +
.../assets/icons/file-types/folder-mock.svg | 1 +
.../icons/file-types/folder-mojo-open.svg | 1 +
.../assets/icons/file-types/folder-mojo.svg | 1 +
.../icons/file-types/folder-molecule-open.svg | 1 +
.../icons/file-types/folder-molecule.svg | 1 +
.../icons/file-types/folder-moon-open.svg | 1 +
.../assets/icons/file-types/folder-moon.svg | 1 +
.../icons/file-types/folder-netlify-open.svg | 1 +
.../icons/file-types/folder-netlify.svg | 1 +
.../icons/file-types/folder-next-open.svg | 6 +
.../assets/icons/file-types/folder-next.svg | 6 +
.../file-types/folder-ngrx-store-open.svg | 1 +
.../icons/file-types/folder-ngrx-store.svg | 1 +
.../icons/file-types/folder-node-open.svg | 1 +
.../assets/icons/file-types/folder-node.svg | 1 +
.../icons/file-types/folder-nuxt-open.svg | 6 +
.../assets/icons/file-types/folder-nuxt.svg | 6 +
.../icons/file-types/folder-obsidian-open.svg | 1 +
.../icons/file-types/folder-obsidian.svg | 1 +
.../assets/icons/file-types/folder-open.svg | 5 +
.../icons/file-types/folder-organism-open.svg | 1 +
.../icons/file-types/folder-organism.svg | 1 +
.../icons/file-types/folder-other-open.svg | 1 +
.../assets/icons/file-types/folder-other.svg | 1 +
.../icons/file-types/folder-packages-open.svg | 1 +
.../icons/file-types/folder-packages.svg | 1 +
.../icons/file-types/folder-pdf-open.svg | 1 +
ui/src/assets/icons/file-types/folder-pdf.svg | 1 +
.../icons/file-types/folder-pdm-open.svg | 1 +
ui/src/assets/icons/file-types/folder-pdm.svg | 1 +
.../icons/file-types/folder-php-open.svg | 1 +
ui/src/assets/icons/file-types/folder-php.svg | 1 +
.../file-types/folder-phpmailer-open.svg | 1 +
.../icons/file-types/folder-phpmailer.svg | 1 +
.../icons/file-types/folder-pipe-open.svg | 1 +
.../assets/icons/file-types/folder-pipe.svg | 1 +
.../icons/file-types/folder-plastic-open.svg | 1 +
.../icons/file-types/folder-plastic.svg | 1 +
.../icons/file-types/folder-plugin-open.svg | 1 +
.../assets/icons/file-types/folder-plugin.svg | 1 +
.../icons/file-types/folder-policy-open.svg | 1 +
.../assets/icons/file-types/folder-policy.svg | 1 +
.../file-types/folder-powershell-open.svg | 1 +
.../icons/file-types/folder-powershell.svg | 1 +
.../icons/file-types/folder-prisma-open.svg | 1 +
.../assets/icons/file-types/folder-prisma.svg | 1 +
.../icons/file-types/folder-private-open.svg | 1 +
.../icons/file-types/folder-private.svg | 1 +
.../icons/file-types/folder-project-open.svg | 1 +
.../icons/file-types/folder-project.svg | 1 +
.../icons/file-types/folder-prompts-open.svg | 1 +
.../icons/file-types/folder-prompts.svg | 1 +
.../icons/file-types/folder-proto-open.svg | 1 +
.../assets/icons/file-types/folder-proto.svg | 1 +
.../icons/file-types/folder-public-open.svg | 1 +
.../assets/icons/file-types/folder-public.svg | 1 +
.../icons/file-types/folder-python-open.svg | 1 +
.../assets/icons/file-types/folder-python.svg | 1 +
.../icons/file-types/folder-pytorch-open.svg | 1 +
.../icons/file-types/folder-pytorch.svg | 1 +
.../icons/file-types/folder-quasar-open.svg | 1 +
.../assets/icons/file-types/folder-quasar.svg | 1 +
.../icons/file-types/folder-queue-open.svg | 1 +
.../assets/icons/file-types/folder-queue.svg | 1 +
.../folder-react-components-open.svg | 1 +
.../file-types/folder-react-components.svg | 1 +
.../file-types/folder-redux-reducer-open.svg | 1 +
.../icons/file-types/folder-redux-reducer.svg | 1 +
.../file-types/folder-repository-open.svg | 1 +
.../icons/file-types/folder-repository.svg | 1 +
.../icons/file-types/folder-resolver-open.svg | 1 +
.../icons/file-types/folder-resolver.svg | 1 +
.../icons/file-types/folder-resource-open.svg | 1 +
.../icons/file-types/folder-resource.svg | 1 +
.../icons/file-types/folder-review-open.svg | 1 +
.../assets/icons/file-types/folder-review.svg | 1 +
.../icons/file-types/folder-robot-open.svg | 1 +
.../assets/icons/file-types/folder-robot.svg | 1 +
.../icons/file-types/folder-routes-open.svg | 1 +
.../assets/icons/file-types/folder-routes.svg | 1 +
.../icons/file-types/folder-rules-open.svg | 1 +
.../assets/icons/file-types/folder-rules.svg | 1 +
.../icons/file-types/folder-rust-open.svg | 1 +
.../assets/icons/file-types/folder-rust.svg | 1 +
.../icons/file-types/folder-sandbox-open.svg | 1 +
.../icons/file-types/folder-sandbox.svg | 1 +
.../icons/file-types/folder-sass-open.svg | 1 +
.../assets/icons/file-types/folder-sass.svg | 1 +
.../icons/file-types/folder-scala-open.svg | 1 +
.../assets/icons/file-types/folder-scala.svg | 1 +
.../icons/file-types/folder-scons-open.svg | 1 +
.../assets/icons/file-types/folder-scons.svg | 1 +
.../icons/file-types/folder-scripts-open.svg | 6 +
.../icons/file-types/folder-scripts.svg | 6 +
.../icons/file-types/folder-secure-open.svg | 1 +
.../assets/icons/file-types/folder-secure.svg | 1 +
.../icons/file-types/folder-seeders-open.svg | 1 +
.../icons/file-types/folder-seeders.svg | 1 +
.../icons/file-types/folder-server-open.svg | 1 +
.../assets/icons/file-types/folder-server.svg | 1 +
.../file-types/folder-serverless-open.svg | 1 +
.../icons/file-types/folder-serverless.svg | 1 +
.../icons/file-types/folder-shader-open.svg | 1 +
.../assets/icons/file-types/folder-shader.svg | 1 +
.../icons/file-types/folder-shared-open.svg | 1 +
.../assets/icons/file-types/folder-shared.svg | 1 +
.../file-types/folder-snapcraft-open.svg | 1 +
.../icons/file-types/folder-snapcraft.svg | 1 +
.../icons/file-types/folder-snippet-open.svg | 1 +
.../icons/file-types/folder-snippet.svg | 1 +
.../icons/file-types/folder-src-open.svg | 1 +
.../file-types/folder-src-tauri-open.svg | 1 +
.../icons/file-types/folder-src-tauri.svg | 1 +
ui/src/assets/icons/file-types/folder-src.svg | 1 +
.../icons/file-types/folder-stack-open.svg | 1 +
.../assets/icons/file-types/folder-stack.svg | 1 +
.../icons/file-types/folder-stencil-open.svg | 1 +
.../icons/file-types/folder-stencil.svg | 1 +
.../icons/file-types/folder-store-open.svg | 1 +
.../assets/icons/file-types/folder-store.svg | 1 +
.../file-types/folder-storybook-open.svg | 1 +
.../icons/file-types/folder-storybook.svg | 1 +
.../icons/file-types/folder-stylus-open.svg | 1 +
.../assets/icons/file-types/folder-stylus.svg | 1 +
.../icons/file-types/folder-sublime-open.svg | 1 +
.../icons/file-types/folder-sublime.svg | 1 +
.../icons/file-types/folder-supabase-open.svg | 1 +
.../icons/file-types/folder-supabase.svg | 1 +
.../icons/file-types/folder-svelte-open.svg | 1 +
.../assets/icons/file-types/folder-svelte.svg | 1 +
.../icons/file-types/folder-svg-open.svg | 1 +
ui/src/assets/icons/file-types/folder-svg.svg | 1 +
.../icons/file-types/folder-syntax-open.svg | 1 +
.../assets/icons/file-types/folder-syntax.svg | 1 +
.../icons/file-types/folder-target-open.svg | 10 +
.../assets/icons/file-types/folder-target.svg | 10 +
.../icons/file-types/folder-taskfile-open.svg | 1 +
.../icons/file-types/folder-taskfile.svg | 1 +
.../icons/file-types/folder-tasks-open.svg | 1 +
.../assets/icons/file-types/folder-tasks.svg | 1 +
.../file-types/folder-television-open.svg | 1 +
.../icons/file-types/folder-television.svg | 1 +
.../icons/file-types/folder-temp-open.svg | 1 +
.../assets/icons/file-types/folder-temp.svg | 1 +
.../icons/file-types/folder-template-open.svg | 1 +
.../icons/file-types/folder-template.svg | 1 +
.../file-types/folder-terraform-open.svg | 1 +
.../icons/file-types/folder-terraform.svg | 1 +
.../icons/file-types/folder-test-open.svg | 1 +
.../assets/icons/file-types/folder-test.svg | 1 +
.../icons/file-types/folder-theme-open.svg | 1 +
.../assets/icons/file-types/folder-theme.svg | 1 +
.../icons/file-types/folder-tools-open.svg | 1 +
.../assets/icons/file-types/folder-tools.svg | 1 +
.../icons/file-types/folder-trash-open.svg | 1 +
.../assets/icons/file-types/folder-trash.svg | 1 +
.../icons/file-types/folder-trigger-open.svg | 1 +
.../icons/file-types/folder-trigger.svg | 1 +
.../file-types/folder-turborepo-open.svg | 15 +
.../icons/file-types/folder-turborepo.svg | 15 +
.../file-types/folder-typescript-open.svg | 1 +
.../icons/file-types/folder-typescript.svg | 1 +
.../icons/file-types/folder-ui-open.svg | 1 +
ui/src/assets/icons/file-types/folder-ui.svg | 1 +
.../icons/file-types/folder-unity-open.svg | 1 +
.../assets/icons/file-types/folder-unity.svg | 1 +
.../icons/file-types/folder-update-open.svg | 1 +
.../assets/icons/file-types/folder-update.svg | 1 +
.../icons/file-types/folder-upload-open.svg | 1 +
.../assets/icons/file-types/folder-upload.svg | 1 +
.../icons/file-types/folder-utils-open.svg | 1 +
.../assets/icons/file-types/folder-utils.svg | 1 +
.../icons/file-types/folder-vercel-open.svg | 5 +
.../assets/icons/file-types/folder-vercel.svg | 5 +
.../file-types/folder-verdaccio-open.svg | 1 +
.../icons/file-types/folder-verdaccio.svg | 1 +
.../icons/file-types/folder-video-open.svg | 1 +
.../assets/icons/file-types/folder-video.svg | 1 +
.../icons/file-types/folder-views-open.svg | 1 +
.../assets/icons/file-types/folder-views.svg | 1 +
.../icons/file-types/folder-vm-open.svg | 1 +
ui/src/assets/icons/file-types/folder-vm.svg | 1 +
.../icons/file-types/folder-vscode-open.svg | 1 +
.../assets/icons/file-types/folder-vscode.svg | 1 +
.../file-types/folder-vue-directives-open.svg | 1 +
.../file-types/folder-vue-directives.svg | 1 +
.../icons/file-types/folder-vue-open.svg | 1 +
ui/src/assets/icons/file-types/folder-vue.svg | 1 +
.../icons/file-types/folder-vuepress-open.svg | 1 +
.../icons/file-types/folder-vuepress.svg | 1 +
.../file-types/folder-vuex-store-open.svg | 1 +
.../icons/file-types/folder-vuex-store.svg | 1 +
.../icons/file-types/folder-wakatime-open.svg | 1 +
.../icons/file-types/folder-wakatime.svg | 1 +
.../icons/file-types/folder-webpack-open.svg | 1 +
.../icons/file-types/folder-webpack.svg | 1 +
.../icons/file-types/folder-windows-open.svg | 1 +
.../icons/file-types/folder-windows.svg | 1 +
.../file-types/folder-wordpress-open.svg | 1 +
.../icons/file-types/folder-wordpress.svg | 1 +
.../icons/file-types/folder-yarn-open.svg | 1 +
.../assets/icons/file-types/folder-yarn.svg | 1 +
.../icons/file-types/folder-zeabur-open.svg | 1 +
.../assets/icons/file-types/folder-zeabur.svg | 1 +
ui/src/assets/icons/file-types/folder.svg | 5 +
ui/src/assets/icons/file-types/font.svg | 1 +
ui/src/assets/icons/file-types/forth.svg | 1 +
ui/src/assets/icons/file-types/fortran.svg | 1 +
ui/src/assets/icons/file-types/foxpro.svg | 1 +
ui/src/assets/icons/file-types/freemarker.svg | 1 +
ui/src/assets/icons/file-types/fsharp.svg | 1 +
ui/src/assets/icons/file-types/fusebox.svg | 1 +
ui/src/assets/icons/file-types/gamemaker.svg | 1 +
ui/src/assets/icons/file-types/garden.svg | 1 +
ui/src/assets/icons/file-types/gatsby.svg | 1 +
ui/src/assets/icons/file-types/gcp.svg | 1 +
ui/src/assets/icons/file-types/gemfile.svg | 1 +
ui/src/assets/icons/file-types/gemini-ai.svg | 1 +
ui/src/assets/icons/file-types/gemini.svg | 1 +
ui/src/assets/icons/file-types/git.svg | 1 +
.../file-types/github-actions-workflow.svg | 1 +
.../icons/file-types/github-sponsors.svg | 1 +
ui/src/assets/icons/file-types/gitlab.svg | 1 +
ui/src/assets/icons/file-types/gitpod.svg | 1 +
ui/src/assets/icons/file-types/gleam.svg | 1 +
ui/src/assets/icons/file-types/gnuplot.svg | 1 +
ui/src/assets/icons/file-types/go-mod.svg | 1 +
ui/src/assets/icons/file-types/go.svg | 1 +
ui/src/assets/icons/file-types/go_gopher.svg | 35 +
.../assets/icons/file-types/godot-assets.svg | 1 +
ui/src/assets/icons/file-types/godot.svg | 1 +
ui/src/assets/icons/file-types/gradle.svg | 1 +
.../assets/icons/file-types/grafana-alloy.svg | 1 +
ui/src/assets/icons/file-types/grain.svg | 1 +
ui/src/assets/icons/file-types/graphcool.svg | 1 +
ui/src/assets/icons/file-types/graphql.svg | 1 +
ui/src/assets/icons/file-types/gridsome.svg | 1 +
ui/src/assets/icons/file-types/groovy.svg | 1 +
ui/src/assets/icons/file-types/grunt.svg | 1 +
ui/src/assets/icons/file-types/gulp.svg | 1 +
ui/src/assets/icons/file-types/h.svg | 1 +
ui/src/assets/icons/file-types/hack.svg | 1 +
ui/src/assets/icons/file-types/hadolint.svg | 1 +
ui/src/assets/icons/file-types/haml.svg | 1 +
ui/src/assets/icons/file-types/handlebars.svg | 1 +
ui/src/assets/icons/file-types/hardhat.svg | 1 +
ui/src/assets/icons/file-types/harmonix.svg | 1 +
ui/src/assets/icons/file-types/haskell.svg | 1 +
ui/src/assets/icons/file-types/haxe.svg | 1 +
ui/src/assets/icons/file-types/hcl.svg | 1 +
ui/src/assets/icons/file-types/hcl_light.svg | 1 +
ui/src/assets/icons/file-types/helm.svg | 1 +
ui/src/assets/icons/file-types/heroku.svg | 1 +
ui/src/assets/icons/file-types/hex.svg | 1 +
ui/src/assets/icons/file-types/histoire.svg | 1 +
ui/src/assets/icons/file-types/hjson.svg | 1 +
ui/src/assets/icons/file-types/horusec.svg | 1 +
ui/src/assets/icons/file-types/hosts.svg | 1 +
.../assets/icons/file-types/hosts_light.svg | 1 +
ui/src/assets/icons/file-types/hpp.svg | 1 +
ui/src/assets/icons/file-types/html.svg | 1 +
ui/src/assets/icons/file-types/http.svg | 1 +
ui/src/assets/icons/file-types/huff.svg | 1 +
ui/src/assets/icons/file-types/huff_light.svg | 1 +
ui/src/assets/icons/file-types/hurl.svg | 1 +
ui/src/assets/icons/file-types/husky.svg | 1 +
ui/src/assets/icons/file-types/i18n.svg | 1 +
ui/src/assets/icons/file-types/idris.svg | 1 +
.../assets/icons/file-types/ifanr-cloud.svg | 1 +
ui/src/assets/icons/file-types/image.svg | 1 +
ui/src/assets/icons/file-types/imba.svg | 1 +
.../assets/icons/file-types/installation.svg | 1 +
ui/src/assets/icons/file-types/ionic.svg | 1 +
ui/src/assets/icons/file-types/istanbul.svg | 1 +
ui/src/assets/icons/file-types/jar.svg | 1 +
ui/src/assets/icons/file-types/java.svg | 1 +
ui/src/assets/icons/file-types/javaclass.svg | 1 +
.../icons/file-types/javascript-map.svg | 1 +
ui/src/assets/icons/file-types/javascript.svg | 1 +
ui/src/assets/icons/file-types/jenkins.svg | 1 +
ui/src/assets/icons/file-types/jest.svg | 1 +
ui/src/assets/icons/file-types/jinja.svg | 1 +
.../assets/icons/file-types/jinja_light.svg | 1 +
ui/src/assets/icons/file-types/jsconfig.svg | 1 +
ui/src/assets/icons/file-types/json.svg | 1 +
ui/src/assets/icons/file-types/jsr.svg | 1 +
ui/src/assets/icons/file-types/jsr_light.svg | 1 +
ui/src/assets/icons/file-types/julia.svg | 1 +
ui/src/assets/icons/file-types/jupyter.svg | 1 +
ui/src/assets/icons/file-types/just.svg | 1 +
ui/src/assets/icons/file-types/karma.svg | 1 +
ui/src/assets/icons/file-types/kcl.svg | 1 +
ui/src/assets/icons/file-types/key.svg | 1 +
ui/src/assets/icons/file-types/keystatic.svg | 1 +
ui/src/assets/icons/file-types/kivy.svg | 1 +
ui/src/assets/icons/file-types/kl.svg | 1 +
ui/src/assets/icons/file-types/knip.svg | 1 +
ui/src/assets/icons/file-types/kotlin.svg | 1 +
ui/src/assets/icons/file-types/kubernetes.svg | 1 +
ui/src/assets/icons/file-types/kusto.svg | 1 +
ui/src/assets/icons/file-types/label.svg | 1 +
ui/src/assets/icons/file-types/laravel.svg | 1 +
ui/src/assets/icons/file-types/latexmk.svg | 1 +
ui/src/assets/icons/file-types/lbx.svg | 1 +
ui/src/assets/icons/file-types/lefthook.svg | 1 +
ui/src/assets/icons/file-types/lerna.svg | 1 +
ui/src/assets/icons/file-types/less.svg | 1 +
ui/src/assets/icons/file-types/liara.svg | 1 +
ui/src/assets/icons/file-types/lib.svg | 1 +
ui/src/assets/icons/file-types/lighthouse.svg | 1 +
ui/src/assets/icons/file-types/lilypond.svg | 1 +
ui/src/assets/icons/file-types/lintstaged.svg | 1 +
ui/src/assets/icons/file-types/liquid.svg | 1 +
ui/src/assets/icons/file-types/lisp.svg | 1 +
ui/src/assets/icons/file-types/livescript.svg | 1 +
ui/src/assets/icons/file-types/lock.svg | 1 +
ui/src/assets/icons/file-types/log.svg | 1 +
ui/src/assets/icons/file-types/lolcode.svg | 1 +
ui/src/assets/icons/file-types/lottie.svg | 1 +
ui/src/assets/icons/file-types/lua.svg | 1 +
ui/src/assets/icons/file-types/luau.svg | 1 +
ui/src/assets/icons/file-types/lyric.svg | 1 +
ui/src/assets/icons/file-types/makefile.svg | 1 +
.../icons/file-types/markdoc-config.svg | 1 +
ui/src/assets/icons/file-types/markdoc.svg | 1 +
ui/src/assets/icons/file-types/markdown.svg | 1 +
.../assets/icons/file-types/markdownlint.svg | 1 +
ui/src/assets/icons/file-types/markojs.svg | 1 +
.../assets/icons/file-types/mathematica.svg | 1 +
ui/src/assets/icons/file-types/matlab.svg | 1 +
ui/src/assets/icons/file-types/maven.svg | 1 +
ui/src/assets/icons/file-types/mdsvex.svg | 1 +
ui/src/assets/icons/file-types/mdx.svg | 1 +
ui/src/assets/icons/file-types/mercurial.svg | 1 +
ui/src/assets/icons/file-types/merlin.svg | 1 +
ui/src/assets/icons/file-types/mermaid.svg | 1 +
ui/src/assets/icons/file-types/meson.svg | 1 +
.../icons/file-types/minecraft-fabric.svg | 1 +
ui/src/assets/icons/file-types/minecraft.svg | 1 +
ui/src/assets/icons/file-types/mint.svg | 1 +
ui/src/assets/icons/file-types/mjml.svg | 1 +
ui/src/assets/icons/file-types/mocha.svg | 1 +
ui/src/assets/icons/file-types/modernizr.svg | 1 +
ui/src/assets/icons/file-types/mojo.svg | 1 +
ui/src/assets/icons/file-types/moon.svg | 1 +
ui/src/assets/icons/file-types/moonscript.svg | 1 +
ui/src/assets/icons/file-types/mxml.svg | 1 +
.../assets/icons/file-types/nano-staged.svg | 1 +
.../icons/file-types/nano-staged_light.svg | 4 +
ui/src/assets/icons/file-types/ndst.svg | 1 +
ui/src/assets/icons/file-types/nest.svg | 1 +
ui/src/assets/icons/file-types/netlify.svg | 1 +
.../assets/icons/file-types/netlify_light.svg | 1 +
ui/src/assets/icons/file-types/next.svg | 1 +
ui/src/assets/icons/file-types/next_light.svg | 1 +
ui/src/assets/icons/file-types/nginx.svg | 1 +
.../assets/icons/file-types/ngrx-actions.svg | 1 +
.../assets/icons/file-types/ngrx-effects.svg | 1 +
.../assets/icons/file-types/ngrx-entity.svg | 1 +
.../assets/icons/file-types/ngrx-reducer.svg | 1 +
.../icons/file-types/ngrx-selectors.svg | 1 +
ui/src/assets/icons/file-types/ngrx-state.svg | 1 +
ui/src/assets/icons/file-types/nim.svg | 1 +
ui/src/assets/icons/file-types/nix.svg | 1 +
ui/src/assets/icons/file-types/nodejs.svg | 1 +
ui/src/assets/icons/file-types/nodejs_alt.svg | 1 +
ui/src/assets/icons/file-types/nodemon.svg | 1 +
ui/src/assets/icons/file-types/npm.svg | 1 +
ui/src/assets/icons/file-types/nuget.svg | 1 +
ui/src/assets/icons/file-types/nunjucks.svg | 1 +
ui/src/assets/icons/file-types/nuxt.svg | 1 +
ui/src/assets/icons/file-types/nx.svg | 1 +
.../assets/icons/file-types/objective-c.svg | 1 +
.../assets/icons/file-types/objective-cpp.svg | 1 +
ui/src/assets/icons/file-types/ocaml.svg | 1 +
ui/src/assets/icons/file-types/odin.svg | 1 +
ui/src/assets/icons/file-types/opa.svg | 9 +
ui/src/assets/icons/file-types/opam.svg | 1 +
ui/src/assets/icons/file-types/openapi.svg | 1 +
.../assets/icons/file-types/openapi_light.svg | 1 +
ui/src/assets/icons/file-types/otne.svg | 1 +
ui/src/assets/icons/file-types/oxlint.svg | 1 +
ui/src/assets/icons/file-types/packship.svg | 1 +
ui/src/assets/icons/file-types/palette.svg | 1 +
ui/src/assets/icons/file-types/panda.svg | 1 +
ui/src/assets/icons/file-types/parcel.svg | 1 +
ui/src/assets/icons/file-types/pascal.svg | 1 +
ui/src/assets/icons/file-types/pawn.svg | 1 +
ui/src/assets/icons/file-types/payload.svg | 1 +
.../assets/icons/file-types/payload_light.svg | 1 +
ui/src/assets/icons/file-types/pdf.svg | 1 +
ui/src/assets/icons/file-types/pdm.svg | 1 +
ui/src/assets/icons/file-types/percy.svg | 1 +
ui/src/assets/icons/file-types/perl.svg | 1 +
.../assets/icons/file-types/php-cs-fixer.svg | 1 +
ui/src/assets/icons/file-types/php.svg | 1 +
.../assets/icons/file-types/php_elephant.svg | 1 +
.../icons/file-types/php_elephant_pink.svg | 1 +
ui/src/assets/icons/file-types/phpstan.svg | 1 +
ui/src/assets/icons/file-types/phpunit.svg | 1 +
ui/src/assets/icons/file-types/pinejs.svg | 1 +
ui/src/assets/icons/file-types/pipeline.svg | 1 +
ui/src/assets/icons/file-types/pkl.svg | 1 +
ui/src/assets/icons/file-types/plastic.svg | 1 +
ui/src/assets/icons/file-types/playwright.svg | 1 +
ui/src/assets/icons/file-types/plop.svg | 1 +
.../assets/icons/file-types/pm2-ecosystem.svg | 1 +
ui/src/assets/icons/file-types/pnpm.svg | 1 +
ui/src/assets/icons/file-types/pnpm_light.svg | 1 +
ui/src/assets/icons/file-types/poetry.svg | 1 +
ui/src/assets/icons/file-types/postcss.svg | 1 +
ui/src/assets/icons/file-types/posthtml.svg | 1 +
ui/src/assets/icons/file-types/powerpoint.svg | 1 +
ui/src/assets/icons/file-types/powershell.svg | 1 +
ui/src/assets/icons/file-types/pre-commit.svg | 1 +
ui/src/assets/icons/file-types/prettier.svg | 1 +
ui/src/assets/icons/file-types/prisma.svg | 1 +
ui/src/assets/icons/file-types/processing.svg | 1 +
ui/src/assets/icons/file-types/prolog.svg | 1 +
ui/src/assets/icons/file-types/prompt.svg | 1 +
ui/src/assets/icons/file-types/proto.svg | 1 +
ui/src/assets/icons/file-types/protractor.svg | 1 +
ui/src/assets/icons/file-types/pug.svg | 1 +
ui/src/assets/icons/file-types/puppet.svg | 1 +
ui/src/assets/icons/file-types/puppeteer.svg | 1 +
ui/src/assets/icons/file-types/purescript.svg | 1 +
.../assets/icons/file-types/python-misc.svg | 1 +
ui/src/assets/icons/file-types/python.svg | 1 +
ui/src/assets/icons/file-types/pytorch.svg | 1 +
ui/src/assets/icons/file-types/qsharp.svg | 1 +
ui/src/assets/icons/file-types/quarto.svg | 5 +
ui/src/assets/icons/file-types/quasar.svg | 1 +
ui/src/assets/icons/file-types/quokka.svg | 1 +
ui/src/assets/icons/file-types/qwik.svg | 1 +
ui/src/assets/icons/file-types/r.svg | 1 +
ui/src/assets/icons/file-types/racket.svg | 1 +
ui/src/assets/icons/file-types/raml.svg | 1 +
ui/src/assets/icons/file-types/razor.svg | 1 +
ui/src/assets/icons/file-types/rbxmk.svg | 1 +
ui/src/assets/icons/file-types/rc.svg | 1 +
ui/src/assets/icons/file-types/react.svg | 1 +
ui/src/assets/icons/file-types/react_ts.svg | 1 +
ui/src/assets/icons/file-types/readme.svg | 1 +
ui/src/assets/icons/file-types/reason.svg | 1 +
ui/src/assets/icons/file-types/red.svg | 1 +
.../assets/icons/file-types/redux-action.svg | 1 +
.../assets/icons/file-types/redux-reducer.svg | 1 +
.../icons/file-types/redux-selector.svg | 1 +
.../assets/icons/file-types/redux-store.svg | 1 +
ui/src/assets/icons/file-types/regedit.svg | 1 +
ui/src/assets/icons/file-types/remark.svg | 1 +
ui/src/assets/icons/file-types/remix.svg | 1 +
.../assets/icons/file-types/remix_light.svg | 1 +
ui/src/assets/icons/file-types/renovate.svg | 1 +
ui/src/assets/icons/file-types/replit.svg | 1 +
.../icons/file-types/rescript-interface.svg | 1 +
ui/src/assets/icons/file-types/rescript.svg | 1 +
ui/src/assets/icons/file-types/restql.svg | 1 +
ui/src/assets/icons/file-types/riot.svg | 1 +
ui/src/assets/icons/file-types/roadmap.svg | 1 +
ui/src/assets/icons/file-types/roblox.svg | 1 +
ui/src/assets/icons/file-types/robot.svg | 1 +
ui/src/assets/icons/file-types/robots.svg | 1 +
ui/src/assets/icons/file-types/rocket.svg | 1 +
ui/src/assets/icons/file-types/rojo.svg | 1 +
ui/src/assets/icons/file-types/rollup.svg | 1 +
ui/src/assets/icons/file-types/rome.svg | 6 +
ui/src/assets/icons/file-types/routing.svg | 1 +
ui/src/assets/icons/file-types/rspec.svg | 1 +
ui/src/assets/icons/file-types/rubocop.svg | 1 +
.../assets/icons/file-types/rubocop_light.svg | 1 +
ui/src/assets/icons/file-types/ruby.svg | 1 +
ui/src/assets/icons/file-types/ruff.svg | 1 +
ui/src/assets/icons/file-types/rust.svg | 1 +
ui/src/assets/icons/file-types/salesforce.svg | 1 +
ui/src/assets/icons/file-types/san.svg | 1 +
ui/src/assets/icons/file-types/sas.svg | 1 +
ui/src/assets/icons/file-types/sass.svg | 1 +
ui/src/assets/icons/file-types/sbt.svg | 1 +
ui/src/assets/icons/file-types/scala.svg | 1 +
ui/src/assets/icons/file-types/scheme.svg | 1 +
ui/src/assets/icons/file-types/scons.svg | 1 +
.../assets/icons/file-types/scons_light.svg | 1 +
.../assets/icons/file-types/screwdriver.svg | 1 +
ui/src/assets/icons/file-types/search.svg | 1 +
.../icons/file-types/semantic-release.svg | 1 +
.../file-types/semantic-release_light.svg | 1 +
ui/src/assets/icons/file-types/semgrep.svg | 1 +
ui/src/assets/icons/file-types/sentry.svg | 1 +
ui/src/assets/icons/file-types/sequelize.svg | 1 +
ui/src/assets/icons/file-types/serverless.svg | 1 +
ui/src/assets/icons/file-types/settings.svg | 1 +
ui/src/assets/icons/file-types/shader.svg | 1 +
.../assets/icons/file-types/silverstripe.svg | 1 +
ui/src/assets/icons/file-types/simulink.svg | 1 +
ui/src/assets/icons/file-types/siyuan.svg | 1 +
ui/src/assets/icons/file-types/sketch.svg | 1 +
ui/src/assets/icons/file-types/slim.svg | 1 +
ui/src/assets/icons/file-types/slint.svg | 1 +
ui/src/assets/icons/file-types/slug.svg | 1 +
ui/src/assets/icons/file-types/smarty.svg | 1 +
ui/src/assets/icons/file-types/sml.svg | 1 +
ui/src/assets/icons/file-types/snakemake.svg | 1 +
ui/src/assets/icons/file-types/snapcraft.svg | 1 +
ui/src/assets/icons/file-types/snowpack.svg | 1 +
.../icons/file-types/snowpack_light.svg | 1 +
ui/src/assets/icons/file-types/snyk.svg | 1 +
ui/src/assets/icons/file-types/solidity.svg | 1 +
ui/src/assets/icons/file-types/sonarcloud.svg | 1 +
ui/src/assets/icons/file-types/sprite.svg | 6509 ++++++++
ui/src/assets/icons/file-types/spwn.svg | 1 +
ui/src/assets/icons/file-types/stackblitz.svg | 1 +
ui/src/assets/icons/file-types/stan.svg | 1 +
ui/src/assets/icons/file-types/steadybit.svg | 1 +
ui/src/assets/icons/file-types/stencil.svg | 1 +
ui/src/assets/icons/file-types/stitches.svg | 1 +
.../icons/file-types/stitches_light.svg | 10 +
ui/src/assets/icons/file-types/storybook.svg | 1 +
ui/src/assets/icons/file-types/stryker.svg | 1 +
ui/src/assets/icons/file-types/stylable.svg | 1 +
ui/src/assets/icons/file-types/stylelint.svg | 1 +
.../icons/file-types/stylelint_light.svg | 13 +
ui/src/assets/icons/file-types/stylus.svg | 1 +
ui/src/assets/icons/file-types/sublime.svg | 1 +
ui/src/assets/icons/file-types/subtitles.svg | 1 +
ui/src/assets/icons/file-types/supabase.svg | 1 +
ui/src/assets/icons/file-types/svelte.svg | 1 +
ui/src/assets/icons/file-types/svg.svg | 1 +
ui/src/assets/icons/file-types/svgo.svg | 1 +
ui/src/assets/icons/file-types/svgr.svg | 1 +
ui/src/assets/icons/file-types/swagger.svg | 1 +
ui/src/assets/icons/file-types/sway.svg | 1 +
ui/src/assets/icons/file-types/swc.svg | 1 +
ui/src/assets/icons/file-types/swift.svg | 1 +
ui/src/assets/icons/file-types/syncpack.svg | 1 +
ui/src/assets/icons/file-types/systemd.svg | 1 +
.../assets/icons/file-types/systemd_light.svg | 1 +
ui/src/assets/icons/file-types/table.svg | 1 +
.../assets/icons/file-types/tailwindcss.svg | 1 +
ui/src/assets/icons/file-types/taskfile.svg | 1 +
ui/src/assets/icons/file-types/tauri.svg | 1 +
ui/src/assets/icons/file-types/taze.svg | 1 +
ui/src/assets/icons/file-types/tcl.svg | 1 +
ui/src/assets/icons/file-types/teal.svg | 1 +
ui/src/assets/icons/file-types/templ.svg | 1 +
ui/src/assets/icons/file-types/template.svg | 1 +
ui/src/assets/icons/file-types/terraform.svg | 1 +
ui/src/assets/icons/file-types/test-js.svg | 1 +
ui/src/assets/icons/file-types/test-jsx.svg | 1 +
ui/src/assets/icons/file-types/test-ts.svg | 1 +
ui/src/assets/icons/file-types/tex.svg | 1 +
ui/src/assets/icons/file-types/textlint.svg | 1 +
ui/src/assets/icons/file-types/tilt.svg | 1 +
ui/src/assets/icons/file-types/tldraw.svg | 1 +
.../assets/icons/file-types/tldraw_light.svg | 1 +
ui/src/assets/icons/file-types/tobi.svg | 1 +
ui/src/assets/icons/file-types/tobimake.svg | 1 +
ui/src/assets/icons/file-types/todo.svg | 1 +
ui/src/assets/icons/file-types/toml.svg | 1 +
ui/src/assets/icons/file-types/toml_light.svg | 1 +
ui/src/assets/icons/file-types/travis.svg | 1 +
ui/src/assets/icons/file-types/tree.svg | 1 +
ui/src/assets/icons/file-types/trigger.svg | 1 +
ui/src/assets/icons/file-types/tsconfig.svg | 1 +
ui/src/assets/icons/file-types/tsdoc.svg | 1 +
ui/src/assets/icons/file-types/tsil.svg | 1 +
ui/src/assets/icons/file-types/tune.svg | 1 +
ui/src/assets/icons/file-types/turborepo.svg | 1 +
.../icons/file-types/turborepo_light.svg | 1 +
ui/src/assets/icons/file-types/twig.svg | 1 +
ui/src/assets/icons/file-types/twine.svg | 1 +
.../icons/file-types/typescript-def.svg | 1 +
ui/src/assets/icons/file-types/typescript.svg | 1 +
ui/src/assets/icons/file-types/typst.svg | 1 +
ui/src/assets/icons/file-types/umi.svg | 1 +
ui/src/assets/icons/file-types/uml.svg | 1 +
ui/src/assets/icons/file-types/uml_light.svg | 1 +
ui/src/assets/icons/file-types/unity.svg | 1 +
ui/src/assets/icons/file-types/unocss.svg | 5 +
ui/src/assets/icons/file-types/url.svg | 1 +
ui/src/assets/icons/file-types/uv.svg | 1 +
ui/src/assets/icons/file-types/vagrant.svg | 1 +
ui/src/assets/icons/file-types/vala.svg | 1 +
.../icons/file-types/vanilla-extract.svg | 1 +
ui/src/assets/icons/file-types/varnish.svg | 1 +
ui/src/assets/icons/file-types/vedic.svg | 1 +
ui/src/assets/icons/file-types/velite.svg | 1 +
ui/src/assets/icons/file-types/velocity.svg | 1 +
ui/src/assets/icons/file-types/vercel.svg | 1 +
.../assets/icons/file-types/vercel_light.svg | 1 +
ui/src/assets/icons/file-types/verdaccio.svg | 1 +
ui/src/assets/icons/file-types/verified.svg | 1 +
ui/src/assets/icons/file-types/verilog.svg | 1 +
ui/src/assets/icons/file-types/vfl.svg | 1 +
ui/src/assets/icons/file-types/video.svg | 1 +
ui/src/assets/icons/file-types/vim.svg | 1 +
ui/src/assets/icons/file-types/virtual.svg | 1 +
.../assets/icons/file-types/visualstudio.svg | 1 +
ui/src/assets/icons/file-types/vite.svg | 1 +
ui/src/assets/icons/file-types/vitest.svg | 1 +
ui/src/assets/icons/file-types/vlang.svg | 6 +
ui/src/assets/icons/file-types/vscode.svg | 1 +
ui/src/assets/icons/file-types/vue-config.svg | 1 +
ui/src/assets/icons/file-types/vue.svg | 1 +
ui/src/assets/icons/file-types/vuex-store.svg | 1 +
ui/src/assets/icons/file-types/wakatime.svg | 1 +
.../icons/file-types/wakatime_light.svg | 1 +
ui/src/assets/icons/file-types/wallaby.svg | 1 +
ui/src/assets/icons/file-types/wally.svg | 1 +
ui/src/assets/icons/file-types/watchman.svg | 1 +
.../assets/icons/file-types/webassembly.svg | 1 +
ui/src/assets/icons/file-types/webhint.svg | 1 +
ui/src/assets/icons/file-types/webpack.svg | 1 +
ui/src/assets/icons/file-types/wepy.svg | 1 +
ui/src/assets/icons/file-types/werf.svg | 1 +
ui/src/assets/icons/file-types/windicss.svg | 1 +
.../icons/file-types/wolframlanguage.svg | 1 +
ui/src/assets/icons/file-types/word.svg | 1 +
ui/src/assets/icons/file-types/wrangler.svg | 1 +
ui/src/assets/icons/file-types/wxt.svg | 1 +
ui/src/assets/icons/file-types/xaml.svg | 1 +
ui/src/assets/icons/file-types/xmake.svg | 1 +
ui/src/assets/icons/file-types/xml.svg | 1 +
ui/src/assets/icons/file-types/yaml.svg | 1 +
ui/src/assets/icons/file-types/yang.svg | 1 +
ui/src/assets/icons/file-types/yarn.svg | 1 +
ui/src/assets/icons/file-types/zeabur.svg | 1 +
.../assets/icons/file-types/zeabur_light.svg | 1 +
ui/src/assets/icons/file-types/zig.svg | 1 +
ui/src/assets/icons/file-types/zip.svg | 1 +
.../provider-logos/bailian-coding-plan.svg | 3 +
ui/src/assets/provider-logos/evroc.svg | 3 +
ui/src/assets/provider-logos/gocode.svg | 2 +
ui/src/components/auth/SessionAuthGate.tsx | 338 +
.../chat/AgentMentionAutocomplete.tsx | 245 +
ui/src/components/chat/ChatContainer.tsx | 686 +
ui/src/components/chat/ChatEmptyState.tsx | 88 +
ui/src/components/chat/ChatErrorBoundary.tsx | 88 +
ui/src/components/chat/ChatInput.tsx | 2822 ++++
ui/src/components/chat/ChatMessage.tsx | 1110 ++
.../components/chat/CommandAutocomplete.tsx | 419 +
ui/src/components/chat/DiffPreview.tsx | 131 +
ui/src/components/chat/FileAttachment.tsx | 606 +
.../chat/FileMentionAutocomplete.tsx | 569 +
ui/src/components/chat/MarkdownRenderer.tsx | 1447 ++
ui/src/components/chat/MessageList.tsx | 1075 ++
ui/src/components/chat/MobileAgentButton.tsx | 94 +
ui/src/components/chat/MobileModelButton.tsx | 37 +
.../chat/MobileSessionStatusBar.tsx | 1598 ++
ui/src/components/chat/ModelControls.tsx | 2852 ++++
ui/src/components/chat/PermissionCard.tsx | 468 +
ui/src/components/chat/PermissionRequest.tsx | 128 +
.../chat/PermissionToastActions.tsx | 131 +
ui/src/components/chat/QuestionCard.tsx | 425 +
ui/src/components/chat/QueuedMessageChips.tsx | 116 +
ui/src/components/chat/SkillAutocomplete.tsx | 172 +
ui/src/components/chat/StatusChip.tsx | 67 +
ui/src/components/chat/StatusRow.tsx | 294 +
ui/src/components/chat/StreamingTextDiff.tsx | 0
ui/src/components/chat/TimelineDialog.tsx | 210 +
.../components/chat/UnifiedControlsDrawer.tsx | 258 +
.../chat/contexts/TurnGroupingContext.tsx | 656 +
.../components/chat/hooks/useTurnGrouping.ts | 517 +
.../chat/message/DiffViewToggle.tsx | 39 +
.../chat/message/FadeInOnReveal.tsx | 69 +
.../components/chat/message/MessageBody.tsx | 1478 ++
.../components/chat/message/MessageHeader.tsx | 102 +
.../chat/message/TextSelectionMenu.tsx | 413 +
.../chat/message/ToolOutputDialog.tsx | 1214 ++
ui/src/components/chat/message/messageRole.ts | 34 +
ui/src/components/chat/message/partUtils.ts | 70 +
.../chat/message/parts/AssistantTextPart.tsx | 82 +
.../chat/message/parts/JustificationBlock.tsx | 53 +
.../chat/message/parts/MigratingPart.tsx | 31 +
.../chat/message/parts/ProgressiveGroup.tsx | 322 +
.../chat/message/parts/ReasoningPart.tsx | 256 +
.../chat/message/parts/ToolPart.tsx | 1938 +++
.../chat/message/parts/UserTextPart.tsx | 164 +
.../message/parts/VirtualizedCodeBlock.tsx | 339 +
.../chat/message/parts/WorkingPlaceholder.tsx | 232 +
ui/src/components/chat/message/timeFormat.ts | 48 +
.../components/chat/message/toolRenderers.tsx | 722 +
ui/src/components/chat/message/types.ts | 37 +
ui/src/components/chat/mobileControlsUtils.ts | 105 +
.../comments/CodeMirrorCommentWidgets.tsx | 106 +
.../components/comments/InlineCommentCard.tsx | 119 +
.../comments/InlineCommentInput.tsx | 179 +
.../comments/PierreDiffCommentOverlays.tsx | 230 +
.../comments/PierreDiffCommentUtils.ts | 52 +
ui/src/components/comments/index.ts | 6 +
.../comments/useInlineCommentController.ts | 154 +
.../desktop/DesktopHostSwitcher.tsx | 1389 ++
ui/src/components/desktop/OpenInAppButton.tsx | 214 +
ui/src/components/icons/ArrowsMerge.tsx | 19 +
ui/src/components/icons/DiffIcon.tsx | 27 +
ui/src/components/icons/FileTypeIcon.tsx | 26 +
ui/src/components/icons/McpIcon.tsx | 20 +
ui/src/components/icons/StopIcon.tsx | 20 +
.../components/layout/BottomTerminalDock.tsx | 193 +
ui/src/components/layout/ContextPanel.tsx | 575 +
.../components/layout/ContextSidebarTab.tsx | 607 +
ui/src/components/layout/Header.tsx | 1932 +++
ui/src/components/layout/MainLayout.tsx | 838 ++
ui/src/components/layout/NavRail.tsx | 919 ++
.../layout/ProjectActionsButton.tsx | 818 +
.../components/layout/ProjectEditDialog.tsx | 433 +
ui/src/components/layout/RightSidebar.tsx | 144 +
ui/src/components/layout/RightSidebarTabs.tsx | 46 +
ui/src/components/layout/Sidebar.tsx | 161 +
.../layout/SidebarContextSummary.tsx | 61 +
ui/src/components/layout/SidebarFilesTree.tsx | 880 ++
ui/src/components/layout/VSCodeLayout.tsx | 731 +
ui/src/components/mcp/McpDropdown.tsx | 412 +
ui/src/components/multirun/AgentSelector.tsx | 109 +
ui/src/components/multirun/BranchSelector.tsx | 249 +
.../components/multirun/ModelMultiSelect.tsx | 552 +
.../components/multirun/MultiRunLauncher.tsx | 645 +
ui/src/components/multirun/index.ts | 4 +
.../onboarding/OnboardingScreen.tsx | 341 +
ui/src/components/providers/ThemeProvider.tsx | 17 +
.../sections/SectionPlaceholder.tsx | 42 +
.../components/sections/agents/AgentsPage.tsx | 1097 ++
.../sections/agents/AgentsSidebar.tsx | 625 +
.../sections/agents/ModelSelector.tsx | 698 +
.../sections/commands/AgentSelector.tsx | 158 +
.../sections/commands/CommandsPage.tsx | 337 +
.../sections/commands/CommandsSidebar.tsx | 484 +
.../GitIdentityEditorDialog.tsx | 478 +
.../sections/git-identities/GitPage.tsx | 349 +
ui/src/components/sections/mcp/McpPage.tsx | 747 +
ui/src/components/sections/mcp/McpSidebar.tsx | 329 +
.../sections/openchamber/AboutSettings.tsx | 224 +
.../sections/openchamber/DefaultsSettings.tsx | 297 +
.../sections/openchamber/GitHubSettings.tsx | 427 +
.../sections/openchamber/GitSettings.tsx | 137 +
.../openchamber/KeyboardShortcutsSettings.tsx | 263 +
.../openchamber/MemoryLimitsSettings.tsx | 128 +
.../openchamber/NotificationSettings.tsx | 921 ++
.../sections/openchamber/OpenChamberPage.tsx | 157 +
.../openchamber/OpenChamberVisualSettings.tsx | 1005 ++
.../openchamber/OpenCodeCliSettings.tsx | 149 +
.../openchamber/SessionRetentionSettings.tsx | 132 +
.../openchamber/WorktreeSectionContent.tsx | 383 +
.../components/sections/openchamber/types.ts | 8 +
.../projects/ProjectActionsSection.tsx | 434 +
.../sections/projects/ProjectsPage.tsx | 499 +
.../sections/projects/ProjectsSidebar.tsx | 150 +
.../sections/providers/ProvidersPage.tsx | 1113 ++
.../sections/providers/ProvidersSidebar.tsx | 213 +
.../remote-instances/RemoteInstancesPage.tsx | 1617 ++
.../RemoteInstancesSidebar.tsx | 218 +
.../sections/shared/SettingsPageLayout.tsx | 48 +
.../shared/SettingsProjectSelector.tsx | 89 +
.../sections/shared/SettingsSection.tsx | 62 +
.../sections/shared/SettingsSidebarHeader.tsx | 63 +
.../sections/shared/SettingsSidebarItem.tsx | 134 +
.../sections/shared/SettingsSidebarLayout.tsx | 118 +
.../sections/shared/SidebarGroup.tsx | 81 +
ui/src/components/sections/shared/index.ts | 57 +
.../components/sections/skills/SkillsPage.tsx | 627 +
.../sections/skills/SkillsSidebar.tsx | 507 +
.../skills/catalog/AddCatalogDialog.tsx | 336 +
.../skills/catalog/InstallConflictsDialog.tsx | 129 +
.../skills/catalog/InstallFromRepoDialog.tsx | 518 +
.../skills/catalog/InstallSkillDialog.tsx | 279 +
.../skills/catalog/SkillsCatalogPage.tsx | 401 +
ui/src/components/sections/skills/index.ts | 2 +
.../sections/skills/skillLocations.ts | 60 +
.../sections/usage/PaceIndicator.tsx | 97 +
.../components/sections/usage/UsageCard.tsx | 92 +
.../components/sections/usage/UsagePage.tsx | 369 +
.../sections/usage/UsageProgressBar.tsx | 54 +
.../sections/usage/UsageSidebar.tsx | 187 +
.../components/session/BranchPickerDialog.tsx | 582 +
.../session/DirectoryAutocomplete.tsx | 314 +
.../session/DirectoryExplorerDialog.tsx | 362 +
ui/src/components/session/DirectoryTree.tsx | 1065 ++
.../session/GitHubIntegrationDialog.tsx | 618 +
.../session/GitHubIssuePickerDialog.tsx | 742 +
.../session/GitHubPrPickerDialog.tsx | 489 +
.../components/session/NewWorktreeDialog.tsx | 1768 +++
.../session/ProjectNotesTodoPanel.tsx | 360 +
ui/src/components/session/SessionDialogs.tsx | 789 +
.../components/session/SessionFolderItem.tsx | 325 +
ui/src/components/session/SessionSidebar.tsx | 1103 ++
.../session/sidebar/ConfirmDialogs.tsx | 113 +
.../session/sidebar/DOCUMENTATION.md | 46 +
.../session/sidebar/SessionGroupSection.tsx | 580 +
.../session/sidebar/SessionNodeItem.tsx | 498 +
.../session/sidebar/SidebarHeader.tsx | 440 +
.../session/sidebar/SidebarProjectsList.tsx | 208 +
.../sidebar/hooks/useArchivedAutoFolders.ts | 120 +
.../sidebar/hooks/useDirectoryStatusProbe.ts | 96 +
.../session/sidebar/hooks/useGroupOrdering.ts | 33 +
.../sidebar/hooks/useProjectRepoStatus.ts | 105 +
.../sidebar/hooks/useProjectSessionLists.ts | 94 +
.../hooks/useProjectSessionSelection.ts | 194 +
.../sidebar/hooks/useSessionActions.ts | 239 +
.../sidebar/hooks/useSessionFolderCleanup.ts | 94 +
.../sidebar/hooks/useSessionGrouping.ts | 237 +
.../sidebar/hooks/useSessionPrefetch.ts | 113 +
.../sidebar/hooks/useSessionSearchEffects.ts | 42 +
.../hooks/useSessionSidebarSections.ts | 176 +
.../sidebar/hooks/useSidebarPersistence.ts | 167 +
.../sidebar/hooks/useStickyProjectHeaders.ts | 50 +
.../session/sidebar/sessionFolderDnd.tsx | 134 +
.../session/sidebar/sortableItems.tsx | 336 +
ui/src/components/session/sidebar/types.ts | 36 +
ui/src/components/session/sidebar/utils.tsx | 242 +
.../components/terminal/TerminalViewport.tsx | 1582 ++
ui/src/components/ui/AboutDialog.tsx | 196 +
ui/src/components/ui/CodeMirrorEditor.tsx | 371 +
ui/src/components/ui/CommandPalette.tsx | 323 +
ui/src/components/ui/ConfigUpdateOverlay.tsx | 27 +
ui/src/components/ui/ContextUsageDisplay.tsx | 172 +
ui/src/components/ui/ErrorBoundary.tsx | 101 +
ui/src/components/ui/FireworksAnimation.tsx | 101 +
ui/src/components/ui/HelpDialog.tsx | 302 +
ui/src/components/ui/MemoryDebugPanel.tsx | 207 +
ui/src/components/ui/MobileOverlayPanel.tsx | 124 +
ui/src/components/ui/OpenChamberLogo.tsx | 277 +
ui/src/components/ui/OpenCodeIcon.tsx | 35 +
ui/src/components/ui/OpenCodeLogo.tsx | 41 +
ui/src/components/ui/OpenCodeStatusDialog.tsx | 59 +
ui/src/components/ui/OverlayScrollbar.tsx | 212 +
ui/src/components/ui/ProviderLogo.tsx | 37 +
ui/src/components/ui/ScrollShadow.tsx | 164 +
ui/src/components/ui/ScrollableOverlay.tsx | 76 +
ui/src/components/ui/TextLoop.tsx | 83 +
ui/src/components/ui/UpdateDialog.tsx | 547 +
ui/src/components/ui/alert.tsx | 66 +
ui/src/components/ui/button-large.tsx | 21 +
ui/src/components/ui/button-small.tsx | 22 +
ui/src/components/ui/button.tsx | 62 +
ui/src/components/ui/card.tsx | 92 +
ui/src/components/ui/checkbox.tsx | 68 +
ui/src/components/ui/collapsible.tsx | 31 +
ui/src/components/ui/command.tsx | 238 +
ui/src/components/ui/dialog.tsx | 160 +
ui/src/components/ui/dropdown-menu.tsx | 265 +
ui/src/components/ui/grid-loader.tsx | 40 +
ui/src/components/ui/index.ts | 2 +
ui/src/components/ui/input.tsx | 26 +
ui/src/components/ui/number-input.tsx | 319 +
ui/src/components/ui/radio.tsx | 69 +
ui/src/components/ui/scroll-area.tsx | 56 +
ui/src/components/ui/select.tsx | 199 +
ui/src/components/ui/separator.tsx | 28 +
ui/src/components/ui/skeleton.tsx | 13 +
ui/src/components/ui/slider.tsx | 64 +
ui/src/components/ui/sonner.tsx | 50 +
ui/src/components/ui/sortable-tabs-strip.tsx | 440 +
ui/src/components/ui/switch.tsx | 29 +
ui/src/components/ui/text.tsx | 211 +
ui/src/components/ui/textarea.tsx | 41 +
ui/src/components/ui/toast.ts | 86 +
ui/src/components/ui/toggle.tsx | 47 +
ui/src/components/ui/tooltip.tsx | 62 +
ui/src/components/ui/typewriter-text.tsx | 61 +
ui/src/components/views/ChatView.tsx | 14 +
ui/src/components/views/DiffView.tsx | 1741 +++
ui/src/components/views/FilesView.tsx | 2987 ++++
ui/src/components/views/GitView.tsx | 2027 +++
ui/src/components/views/PierreDiffViewer.tsx | 734 +
ui/src/components/views/PlanView.tsx | 551 +
.../components/views/PreviewToggleButton.tsx | 51 +
ui/src/components/views/SettingsView.tsx | 766 +
ui/src/components/views/SettingsWindow.tsx | 70 +
ui/src/components/views/TerminalView.tsx | 1098 ++
.../views/agent-manager/AgentGroupDetail.tsx | 342 +
.../agent-manager/AgentManagerEmptyState.tsx | 509 +
.../agent-manager/AgentManagerSidebar.tsx | 286 +
.../views/agent-manager/AgentManagerView.tsx | 199 +
.../components/views/agent-manager/index.ts | 3 +
.../components/views/git/AIHighlightsBox.tsx | 56 +
.../views/git/BranchIntegrationSection.tsx | 468 +
.../components/views/git/BranchSelector.tsx | 346 +
ui/src/components/views/git/ChangeRow.tsx | 183 +
.../components/views/git/ChangesSection.tsx | 251 +
ui/src/components/views/git/CommitInput.tsx | 61 +
ui/src/components/views/git/CommitSection.tsx | 217 +
.../components/views/git/ConflictDialog.tsx | 273 +
ui/src/components/views/git/GitEmptyState.tsx | 42 +
ui/src/components/views/git/GitHeader.tsx | 296 +
.../components/views/git/HistoryCommitRow.tsx | 160 +
.../components/views/git/HistorySection.tsx | 137 +
.../views/git/InProgressOperationBanner.tsx | 152 +
.../views/git/IntegrateCommitsSection.tsx | 524 +
.../views/git/PullRequestSection.tsx | 2021 +++
ui/src/components/views/git/StashDialog.tsx | 128 +
ui/src/components/views/git/SyncActions.tsx | 215 +
.../views/git/WorktreeBranchDisplay.tsx | 142 +
ui/src/components/views/git/index.ts | 14 +
ui/src/components/views/index.ts | 8 +
ui/src/constants/sidebar.ts | 85 +
ui/src/contexts/DiffWorkerProvider.tsx | 142 +
ui/src/contexts/DrawerContext.tsx | 41 +
ui/src/contexts/FireworksContext.tsx | 35 +
ui/src/contexts/RuntimeAPIProvider.tsx | 7 +
ui/src/contexts/ThemeSystemContext.tsx | 769 +
ui/src/contexts/runtimeAPIContext.ts | 4 +
ui/src/contexts/runtimeAPIRegistry.ts | 9 +
ui/src/contexts/theme-system-context.ts | 21 +
ui/src/contexts/useThemeSystem.ts | 15 +
ui/src/hooks/useAssistantStatus.ts | 412 +
ui/src/hooks/useAssistantTyping.ts | 229 +
ui/src/hooks/useAvailableTools.ts | 49 +
ui/src/hooks/useChatScrollManager.ts | 383 +
ui/src/hooks/useChatSearchDirectory.ts | 42 +
ui/src/hooks/useDebouncedValue.ts | 17 +
ui/src/hooks/useDrawerSwipe.ts | 194 +
ui/src/hooks/useEdgeSwipe.ts | 132 +
ui/src/hooks/useEffectiveDirectory.ts | 48 +
ui/src/hooks/useEventStream.ts | 2463 +++
ui/src/hooks/useFileSystemAccess.ts | 41 +
ui/src/hooks/useFireworks.ts | 53 +
ui/src/hooks/useFontPreferences.ts | 13 +
ui/src/hooks/useGitHubPrBackgroundTracking.ts | 382 +
ui/src/hooks/useGitPolling.tsx | 10 +
ui/src/hooks/useGitPollingHook.ts | 44 +
ui/src/hooks/useIsTextTruncated.ts | 46 +
ui/src/hooks/useKeyboardShortcuts.ts | 455 +
ui/src/hooks/useLongPress.ts | 100 +
ui/src/hooks/useMenuActions.ts | 308 +
ui/src/hooks/useModelLists.ts | 58 +
ui/src/hooks/useProviderLogo.ts | 96 +
ui/src/hooks/usePushVisibilityBeacon.ts | 63 +
ui/src/hooks/useQueuedMessageAutoSend.ts | 196 +
ui/src/hooks/useRouter.ts | 347 +
ui/src/hooks/useRuntimeAPIs.ts | 18 +
ui/src/hooks/useScrollEngine.ts | 226 +
ui/src/hooks/useServerSessionStatus.ts | 320 +
ui/src/hooks/useSessionActivity.ts | 57 +
ui/src/hooks/useSessionAutoCleanup.ts | 187 +
ui/src/hooks/useSessionStatusBootstrap.ts | 47 +
ui/src/hooks/useWindowTitle.ts | 135 +
ui/src/index.css | 1344 ++
ui/src/lib/agentColors.ts | 37 +
ui/src/lib/api/types.ts | 1060 ++
ui/src/lib/appearanceAutoSave.ts | 202 +
ui/src/lib/appearancePersistence.ts | 77 +
ui/src/lib/clipboard.ts | 39 +
ui/src/lib/codeTheme.ts | 352 +
ui/src/lib/codemirror/flexokiTheme.ts | 271 +
ui/src/lib/codemirror/languageByExtension.ts | 277 +
ui/src/lib/configSync.ts | 62 +
ui/src/lib/configUpdate.ts | 69 +
ui/src/lib/contextFileOpenGuard.ts | 66 +
ui/src/lib/debug.ts | 706 +
ui/src/lib/desktop.ts | 588 +
ui/src/lib/desktopHosts.ts | 181 +
ui/src/lib/desktopSsh.ts | 454 +
ui/src/lib/device.ts | 221 +
ui/src/lib/diff/workerFactory.ts | 5 +
ui/src/lib/directoryPersistence.ts | 28 +
ui/src/lib/directoryShowHidden.ts | 71 +
ui/src/lib/execCommands.ts | 57 +
ui/src/lib/fileOpenLimits.ts | 19 +
ui/src/lib/fileTypeIconIds.ts | 2088 +++
ui/src/lib/fileTypeIcons.ts | 212 +
ui/src/lib/filesViewShowGitignored.ts | 69 +
ui/src/lib/fontOptions.ts | 38 +
ui/src/lib/git/branchNameGenerator.ts | 76 +
ui/src/lib/git/integrateWorktreeCommits.ts | 359 +
ui/src/lib/gitApi.ts | 703 +
ui/src/lib/gitApiHttp.ts | 762 +
ui/src/lib/ime.ts | 14 +
ui/src/lib/messageCompletion.ts | 109 +
ui/src/lib/messageCursorPersistence.ts | 157 +
ui/src/lib/messageFreshness.ts | 90 +
ui/src/lib/messages/agentMentions.ts | 76 +
ui/src/lib/messages/executionMeta.ts | 16 +
ui/src/lib/messages/inlineComments.ts | 58 +
ui/src/lib/messages/messageText.ts | 13 +
ui/src/lib/messages/providerAuthError.ts | 34 +
ui/src/lib/messages/synthetic.ts | 47 +
ui/src/lib/modelPrefsAutoSave.ts | 76 +
ui/src/lib/openCodeStatus.ts | 345 +
ui/src/lib/openInApps.ts | 45 +
ui/src/lib/openchamberConfig.ts | 694 +
ui/src/lib/opencode/client.ts | 2182 +++
ui/src/lib/permissions/editModeColors.ts | 30 +
.../lib/permissions/editPermissionDefaults.ts | 101 +
ui/src/lib/persistence.ts | 873 ++
ui/src/lib/projectActions.ts | 199 +
ui/src/lib/projectMeta.ts | 86 +
ui/src/lib/quota/index.ts | 14 +
ui/src/lib/quota/model-families.ts | 146 +
ui/src/lib/quota/providers/base.ts | 8 +
ui/src/lib/quota/providers/index.ts | 27 +
ui/src/lib/quota/utils.ts | 244 +
ui/src/lib/router/index.ts | 31 +
ui/src/lib/router/parseRoute.ts | 136 +
ui/src/lib/router/serializeRoute.ts | 148 +
ui/src/lib/router/types.ts | 55 +
ui/src/lib/sessionEvents.ts | 57 +
ui/src/lib/settings/metadata.ts | 208 +
ui/src/lib/shiki/appThemeRegistry.ts | 95 +
ui/src/lib/shiki/textMateThemeFromAppTheme.ts | 482 +
ui/src/lib/shiki/vscodeTextMateTheme.ts | 14 +
ui/src/lib/shortcuts.ts | 597 +
ui/src/lib/terminal/SerializeAddon.ts | 497 +
ui/src/lib/terminalApi.ts | 784 +
ui/src/lib/terminalTheme.ts | 143 +
ui/src/lib/theme/cssGenerator.ts | 772 +
ui/src/lib/theme/syntaxThemeGenerator.ts | 208 +
ui/src/lib/theme/themes/aura-dark.json | 183 +
ui/src/lib/theme/themes/aura-light.json | 183 +
ui/src/lib/theme/themes/ayu-dark.json | 183 +
ui/src/lib/theme/themes/ayu-light.json | 183 +
ui/src/lib/theme/themes/carbonfox-dark.json | 183 +
ui/src/lib/theme/themes/carbonfox-light.json | 183 +
ui/src/lib/theme/themes/catppuccin-dark.json | 183 +
ui/src/lib/theme/themes/catppuccin-light.json | 183 +
ui/src/lib/theme/themes/dracula-dark.json | 183 +
ui/src/lib/theme/themes/dracula-light.json | 183 +
ui/src/lib/theme/themes/flexoki-dark.json | 183 +
ui/src/lib/theme/themes/flexoki-light.json | 183 +
ui/src/lib/theme/themes/gruvbox-dark.json | 183 +
ui/src/lib/theme/themes/gruvbox-light.json | 183 +
ui/src/lib/theme/themes/index.ts | 35 +
ui/src/lib/theme/themes/kanagawa-dark.json | 183 +
ui/src/lib/theme/themes/kanagawa-light.json | 183 +
ui/src/lib/theme/themes/mono-dark.json | 183 +
ui/src/lib/theme/themes/mono-light.json | 183 +
ui/src/lib/theme/themes/mono-plus-dark.json | 162 +
ui/src/lib/theme/themes/mono-plus-light.json | 162 +
ui/src/lib/theme/themes/monokai-dark.json | 183 +
ui/src/lib/theme/themes/monokai-light.json | 183 +
ui/src/lib/theme/themes/nightowl-dark.json | 183 +
ui/src/lib/theme/themes/nightowl-light.json | 183 +
ui/src/lib/theme/themes/nord-dark.json | 183 +
ui/src/lib/theme/themes/nord-light.json | 183 +
ui/src/lib/theme/themes/onedarkpro-dark.json | 183 +
ui/src/lib/theme/themes/onedarkpro-light.json | 183 +
ui/src/lib/theme/themes/prColors.ts | 39 +
ui/src/lib/theme/themes/presets.ts | 74 +
ui/src/lib/theme/themes/solarized-dark.json | 183 +
ui/src/lib/theme/themes/solarized-light.json | 183 +
ui/src/lib/theme/themes/tokyonight-dark.json | 183 +
ui/src/lib/theme/themes/tokyonight-light.json | 183 +
ui/src/lib/theme/themes/vesper-dark.json | 183 +
ui/src/lib/theme/themes/vesper-light.json | 183 +
.../lib/theme/themes/vitesse-dark-dark.json | 166 +
.../lib/theme/themes/vitesse-light-light.json | 166 +
ui/src/lib/theme/vscode/adapter.ts | 548 +
ui/src/lib/toolHelpers.ts | 749 +
ui/src/lib/typography.ts | 357 +
ui/src/lib/typographyWatcher.ts | 37 +
ui/src/lib/utils.ts | 152 +
ui/src/lib/worktreeSessionCreator.ts | 684 +
ui/src/lib/worktrees/worktreeCreate.ts | 120 +
ui/src/lib/worktrees/worktreeManager.ts | 212 +
ui/src/lib/worktrees/worktreeStatus.ts | 97 +
ui/src/main.tsx | 101 +
ui/src/stores/contextStore.ts | 678 +
ui/src/stores/fileStore.ts | 271 +
ui/src/stores/globalSessions.ts | 96 +
ui/src/stores/messageQueueStore.ts | 144 +
ui/src/stores/messageStore.ts | 2854 ++++
ui/src/stores/permissionStore.ts | 160 +
ui/src/stores/questionStore.ts | 123 +
ui/src/stores/sessionStore.ts | 1694 +++
ui/src/stores/types/sessionTypes.ts | 296 +
ui/src/stores/useAgentGroupsStore.ts | 737 +
ui/src/stores/useAgentsStore.ts | 664 +
ui/src/stores/useCommandsStore.ts | 506 +
ui/src/stores/useConfigStore.ts | 1563 ++
ui/src/stores/useDesktopSshStore.ts | 191 +
ui/src/stores/useDirectoryStore.ts | 438 +
ui/src/stores/useFileSearchStore.ts | 151 +
ui/src/stores/useFilesViewTabsStore.ts | 419 +
ui/src/stores/useGitHubAuthStore.ts | 62 +
ui/src/stores/useGitHubPrStatusStore.ts | 694 +
ui/src/stores/useGitIdentitiesStore.ts | 285 +
ui/src/stores/useGitStore.ts | 763 +
ui/src/stores/useInlineCommentDraftStore.ts | 153 +
ui/src/stores/useMcpConfigStore.ts | 288 +
ui/src/stores/useMcpStore.ts | 117 +
ui/src/stores/useMultiRunStore.ts | 283 +
ui/src/stores/useOpenInAppsStore.ts | 281 +
ui/src/stores/useProjectsStore.ts | 702 +
ui/src/stores/useQuotaStore.ts | 298 +
ui/src/stores/useSessionDisplayStore.ts | 21 +
ui/src/stores/useSessionFoldersStore.ts | 498 +
ui/src/stores/useSessionStore.ts | 1101 ++
ui/src/stores/useSkillsCatalogStore.ts | 411 +
ui/src/stores/useSkillsStore.ts | 545 +
ui/src/stores/useTerminalStore.ts | 550 +
ui/src/stores/useTodoStore.ts | 98 +
ui/src/stores/useUIStore.ts | 1877 +++
ui/src/stores/useUpdateStore.ts | 164 +
ui/src/stores/utils/contextUtils.ts | 22 +
ui/src/stores/utils/messageUtils.ts | 68 +
ui/src/stores/utils/permissionUtils.ts | 94 +
ui/src/stores/utils/safeStorage.ts | 224 +
ui/src/stores/utils/streamDebug.ts | 17 +
ui/src/stores/utils/streamingUtils.ts | 55 +
ui/src/stores/utils/tokenUtils.ts | 51 +
ui/src/styles/design-system.css | 381 +
ui/src/styles/fireworks.css | 76 +
ui/src/styles/fonts.ts | 9 +
ui/src/styles/mobile.css | 488 +
ui/src/styles/typography.css | 138 +
ui/src/types/codemirror-lang-elixir.d.ts | 5 +
ui/src/types/desktop.d.ts | 9 +
ui/src/types/electron-context-menu.d.ts | 35 +
ui/src/types/ghostty-web.d.ts | 11 +
ui/src/types/index.ts | 73 +
ui/src/types/multirun.ts | 48 +
ui/src/types/permission.ts | 28 +
ui/src/types/question.ts | 45 +
ui/src/types/quota.ts | 43 +
...act-syntax-highlighter-create-element.d.ts | 14 +
ui/src/types/streamdown.d.ts | 36 +
ui/src/types/theme.ts | 299 +
ui/src/types/tool.ts | 53 +
ui/src/types/vscode.d.ts | 11 +
ui/src/types/worktree.ts | 38 +
ui/src/vite-env.d.ts | 16 +
ui/tsconfig.json | 23 +
vite-theme-plugin.ts | 8 +
vite.config.ts | 57 +
web/.gitignore | 2 +
web/README.md | 89 +
web/bin/cli.js | 1022 ++
web/electron-package.json | 20 +
web/electron/main.js | 68 +
web/electron/package-lock.json | 4885 ++++++
web/electron/package.json | 39 +
web/index.html | 276 +
web/package-electron.json | 38 +
web/package.json | 104 +
web/public/apple-touch-icon-120x120.png | Bin 0 -> 3250 bytes
web/public/apple-touch-icon-152x152.png | Bin 0 -> 4329 bytes
web/public/apple-touch-icon-167x167.png | Bin 0 -> 4806 bytes
web/public/apple-touch-icon-180x180.png | Bin 0 -> 5081 bytes
web/public/apple-touch-icon.png | Bin 0 -> 5081 bytes
web/public/apple-touch-icon.svg | 16 +
web/public/favicon-16.png | Bin 0 -> 437 bytes
web/public/favicon-32.png | Bin 0 -> 919 bytes
web/public/favicon.png | Bin 0 -> 2050 bytes
web/public/favicon.svg | 26 +
web/public/logo-dark-192x192.png | Bin 0 -> 3863 bytes
web/public/logo-dark-512x512.svg | 16 +
web/public/logo-light-192x192.png | Bin 0 -> 3827 bytes
web/public/logo-light-512x512.svg | 16 +
web/server/TERMINAL_INPUT_WS_PROTOCOL.md | 44 +
web/server/index.d.ts | 28 +
web/server/index.js | 12329 ++++++++++++++++
web/server/lib/git/DOCUMENTATION.md | 145 +
web/server/lib/git/credentials.js | 74 +
web/server/lib/git/identity-storage.js | 110 +
web/server/lib/git/index.js | 6 +
web/server/lib/git/service.js | 2860 ++++
web/server/lib/github/DOCUMENTATION.md | 170 +
web/server/lib/github/auth.js | 307 +
web/server/lib/github/device-flow.js | 50 +
web/server/lib/github/index.js | 24 +
web/server/lib/github/octokit.js | 10 +
web/server/lib/github/pr-status.js | 478 +
web/server/lib/github/repo/index.js | 55 +
web/server/lib/notifications/DOCUMENTATION.md | 61 +
web/server/lib/notifications/index.js | 1 +
web/server/lib/notifications/message.js | 49 +
web/server/lib/notifications/message.test.js | 59 +
web/server/lib/opencode/DOCUMENTATION.md | 58 +
web/server/lib/opencode/agents.js | 634 +
web/server/lib/opencode/auth.js | 81 +
web/server/lib/opencode/commands.js | 339 +
web/server/lib/opencode/index.js | 66 +
web/server/lib/opencode/mcp.js | 206 +
web/server/lib/opencode/providers.js | 96 +
web/server/lib/opencode/shared.js | 530 +
web/server/lib/opencode/skills.js | 480 +
web/server/lib/opencode/ui-auth.js | 510 +
web/server/lib/package-manager.js | 362 +
web/server/lib/quota/DOCUMENTATION.md | 55 +
web/server/lib/quota/index.js | 24 +
web/server/lib/quota/providers/claude.js | 107 +
web/server/lib/quota/providers/codex.js | 113 +
web/server/lib/quota/providers/copilot.js | 165 +
web/server/lib/quota/providers/google/api.js | 92 +
web/server/lib/quota/providers/google/auth.js | 108 +
.../lib/quota/providers/google/index.js | 124 +
.../lib/quota/providers/google/transforms.js | 109 +
web/server/lib/quota/providers/index.js | 152 +
web/server/lib/quota/providers/interface.js | 55 +
web/server/lib/quota/providers/kimi.js | 108 +
.../quota/providers/minimax-cn-coding-plan.js | 131 +
.../quota/providers/minimax-coding-plan.js | 131 +
web/server/lib/quota/providers/nanogpt.js | 124 +
.../lib/quota/providers/ollama-cloud.js | 112 +
web/server/lib/quota/providers/openai.js | 91 +
web/server/lib/quota/providers/openrouter.js | 92 +
web/server/lib/quota/providers/zai.js | 91 +
web/server/lib/quota/utils/auth.js | 46 +
web/server/lib/quota/utils/formatters.js | 76 +
web/server/lib/quota/utils/index.js | 10 +
web/server/lib/quota/utils/transformers.js | 55 +
.../lib/skills-catalog/DOCUMENTATION.md | 178 +
web/server/lib/skills-catalog/cache.js | 29 +
web/server/lib/skills-catalog/clawdhub/api.js | 158 +
.../lib/skills-catalog/clawdhub/index.js | 30 +
.../lib/skills-catalog/clawdhub/install.js | 238 +
.../lib/skills-catalog/clawdhub/scan.js | 113 +
.../lib/skills-catalog/curated-sources.js | 21 +
web/server/lib/skills-catalog/git.js | 76 +
web/server/lib/skills-catalog/index.js | 42 +
web/server/lib/skills-catalog/install.js | 294 +
web/server/lib/skills-catalog/scan.js | 221 +
web/server/lib/skills-catalog/source.js | 85 +
web/server/lib/terminal/DOCUMENTATION.md | 114 +
web/server/lib/terminal/index.js | 12 +
web/server/lib/terminal/input-ws-protocol.js | 66 +
.../lib/terminal/input-ws-protocol.test.js | 138 +
web/src/api/files.ts | 197 +
web/src/api/git.ts | 60 +
web/src/api/github.ts | 233 +
web/src/api/index.ts | 23 +
web/src/api/notifications.ts | 77 +
web/src/api/permissions.ts | 15 +
web/src/api/push.ts | 59 +
web/src/api/settings.ts | 58 +
web/src/api/terminal.ts | 74 +
web/src/api/tools.ts | 22 +
web/src/main.tsx | 15 +
web/tsconfig.json | 25 +
web/vite.config.ts | 107 +
1708 files changed, 199890 insertions(+)
create mode 100644 .gitignore
create mode 100644 .nvmrc
create mode 100644 AGENTS.md
create mode 100644 CHANGELOG.md
create mode 100644 CONTRIBUTING.md
create mode 100644 LICENSE
create mode 100644 SECURITY.md
create mode 100644 bun.lock
create mode 100644 components.json
create mode 100644 eslint.config.js
create mode 100644 openchamber.sed
create mode 100644 package.json
create mode 100644 postcss.config.js
create mode 100644 run.bat
create mode 100644 tsconfig.json
create mode 100644 ui/package.json
create mode 100644 ui/src/App.css
create mode 100644 ui/src/App.tsx
create mode 100644 ui/src/assets/icons/file-types/3d.svg
create mode 100644 ui/src/assets/icons/file-types/README.md
create mode 100644 ui/src/assets/icons/file-types/abap.svg
create mode 100644 ui/src/assets/icons/file-types/abc.svg
create mode 100644 ui/src/assets/icons/file-types/actionscript.svg
create mode 100644 ui/src/assets/icons/file-types/ada.svg
create mode 100644 ui/src/assets/icons/file-types/adobe-illustrator.svg
create mode 100644 ui/src/assets/icons/file-types/adobe-illustrator_light.svg
create mode 100644 ui/src/assets/icons/file-types/adobe-photoshop.svg
create mode 100644 ui/src/assets/icons/file-types/adobe-photoshop_light.svg
create mode 100644 ui/src/assets/icons/file-types/adobe-swc.svg
create mode 100644 ui/src/assets/icons/file-types/adonis.svg
create mode 100644 ui/src/assets/icons/file-types/advpl.svg
create mode 100644 ui/src/assets/icons/file-types/amplify.svg
create mode 100644 ui/src/assets/icons/file-types/android.svg
create mode 100644 ui/src/assets/icons/file-types/angular.svg
create mode 100644 ui/src/assets/icons/file-types/antlr.svg
create mode 100644 ui/src/assets/icons/file-types/apiblueprint.svg
create mode 100644 ui/src/assets/icons/file-types/apollo.svg
create mode 100644 ui/src/assets/icons/file-types/applescript.svg
create mode 100644 ui/src/assets/icons/file-types/apps-script.svg
create mode 100644 ui/src/assets/icons/file-types/appveyor.svg
create mode 100644 ui/src/assets/icons/file-types/architecture.svg
create mode 100644 ui/src/assets/icons/file-types/arduino.svg
create mode 100644 ui/src/assets/icons/file-types/asciidoc.svg
create mode 100644 ui/src/assets/icons/file-types/assembly.svg
create mode 100644 ui/src/assets/icons/file-types/astro-config.svg
create mode 100644 ui/src/assets/icons/file-types/astro.svg
create mode 100644 ui/src/assets/icons/file-types/astyle.svg
create mode 100644 ui/src/assets/icons/file-types/audio.svg
create mode 100644 ui/src/assets/icons/file-types/aurelia.svg
create mode 100644 ui/src/assets/icons/file-types/authors.svg
create mode 100644 ui/src/assets/icons/file-types/auto.svg
create mode 100644 ui/src/assets/icons/file-types/auto_light.svg
create mode 100644 ui/src/assets/icons/file-types/autohotkey.svg
create mode 100644 ui/src/assets/icons/file-types/autoit.svg
create mode 100644 ui/src/assets/icons/file-types/azure-pipelines.svg
create mode 100644 ui/src/assets/icons/file-types/azure.svg
create mode 100644 ui/src/assets/icons/file-types/babel.svg
create mode 100644 ui/src/assets/icons/file-types/ballerina.svg
create mode 100644 ui/src/assets/icons/file-types/bazel.svg
create mode 100644 ui/src/assets/icons/file-types/bbx.svg
create mode 100644 ui/src/assets/icons/file-types/beancount.svg
create mode 100644 ui/src/assets/icons/file-types/bench-js.svg
create mode 100644 ui/src/assets/icons/file-types/bench-jsx.svg
create mode 100644 ui/src/assets/icons/file-types/bench-ts.svg
create mode 100644 ui/src/assets/icons/file-types/bibliography.svg
create mode 100644 ui/src/assets/icons/file-types/bibtex-style.svg
create mode 100644 ui/src/assets/icons/file-types/bicep.svg
create mode 100644 ui/src/assets/icons/file-types/biome.svg
create mode 100644 ui/src/assets/icons/file-types/bitbucket.svg
create mode 100644 ui/src/assets/icons/file-types/bithound.svg
create mode 100644 ui/src/assets/icons/file-types/blender.svg
create mode 100644 ui/src/assets/icons/file-types/blink.svg
create mode 100644 ui/src/assets/icons/file-types/blink_light.svg
create mode 100644 ui/src/assets/icons/file-types/blitz.svg
create mode 100644 ui/src/assets/icons/file-types/bower.svg
create mode 100644 ui/src/assets/icons/file-types/brainfuck.svg
create mode 100644 ui/src/assets/icons/file-types/browserlist.svg
create mode 100644 ui/src/assets/icons/file-types/browserlist_light.svg
create mode 100644 ui/src/assets/icons/file-types/bruno.svg
create mode 100644 ui/src/assets/icons/file-types/buck.svg
create mode 100644 ui/src/assets/icons/file-types/bucklescript.svg
create mode 100644 ui/src/assets/icons/file-types/buildkite.svg
create mode 100644 ui/src/assets/icons/file-types/bun.svg
create mode 100644 ui/src/assets/icons/file-types/bun_light.svg
create mode 100644 ui/src/assets/icons/file-types/c.svg
create mode 100644 ui/src/assets/icons/file-types/c3.svg
create mode 100644 ui/src/assets/icons/file-types/cabal.svg
create mode 100644 ui/src/assets/icons/file-types/caddy.svg
create mode 100644 ui/src/assets/icons/file-types/cadence.svg
create mode 100644 ui/src/assets/icons/file-types/cairo.svg
create mode 100644 ui/src/assets/icons/file-types/cake.svg
create mode 100644 ui/src/assets/icons/file-types/capacitor.svg
create mode 100644 ui/src/assets/icons/file-types/capnp.svg
create mode 100644 ui/src/assets/icons/file-types/cbx.svg
create mode 100644 ui/src/assets/icons/file-types/cds.svg
create mode 100644 ui/src/assets/icons/file-types/certificate.svg
create mode 100644 ui/src/assets/icons/file-types/changelog.svg
create mode 100644 ui/src/assets/icons/file-types/chess.svg
create mode 100644 ui/src/assets/icons/file-types/chess_light.svg
create mode 100644 ui/src/assets/icons/file-types/chrome.svg
create mode 100644 ui/src/assets/icons/file-types/circleci.svg
create mode 100644 ui/src/assets/icons/file-types/circleci_light.svg
create mode 100644 ui/src/assets/icons/file-types/citation.svg
create mode 100644 ui/src/assets/icons/file-types/clangd.svg
create mode 100644 ui/src/assets/icons/file-types/claude.svg
create mode 100644 ui/src/assets/icons/file-types/cline.svg
create mode 100644 ui/src/assets/icons/file-types/clojure.svg
create mode 100644 ui/src/assets/icons/file-types/cloudfoundry.svg
create mode 100644 ui/src/assets/icons/file-types/cmake.svg
create mode 100644 ui/src/assets/icons/file-types/coala.svg
create mode 100644 ui/src/assets/icons/file-types/cobol.svg
create mode 100644 ui/src/assets/icons/file-types/coconut.svg
create mode 100644 ui/src/assets/icons/file-types/code-climate.svg
create mode 100644 ui/src/assets/icons/file-types/code-climate_light.svg
create mode 100644 ui/src/assets/icons/file-types/codecov.svg
create mode 100644 ui/src/assets/icons/file-types/codeowners.svg
create mode 100644 ui/src/assets/icons/file-types/coderabbit-ai.svg
create mode 100644 ui/src/assets/icons/file-types/coffee.svg
create mode 100644 ui/src/assets/icons/file-types/coldfusion.svg
create mode 100644 ui/src/assets/icons/file-types/coloredpetrinets.svg
create mode 100644 ui/src/assets/icons/file-types/command.svg
create mode 100644 ui/src/assets/icons/file-types/commitizen.svg
create mode 100644 ui/src/assets/icons/file-types/commitlint.svg
create mode 100644 ui/src/assets/icons/file-types/concourse.svg
create mode 100644 ui/src/assets/icons/file-types/conduct.svg
create mode 100644 ui/src/assets/icons/file-types/console.svg
create mode 100644 ui/src/assets/icons/file-types/contentlayer.svg
create mode 100644 ui/src/assets/icons/file-types/context.svg
create mode 100644 ui/src/assets/icons/file-types/contributing.svg
create mode 100644 ui/src/assets/icons/file-types/controller.svg
create mode 100644 ui/src/assets/icons/file-types/copilot.svg
create mode 100644 ui/src/assets/icons/file-types/copilot_light.svg
create mode 100644 ui/src/assets/icons/file-types/cpp.svg
create mode 100644 ui/src/assets/icons/file-types/craco.svg
create mode 100644 ui/src/assets/icons/file-types/credits.svg
create mode 100644 ui/src/assets/icons/file-types/crystal.svg
create mode 100644 ui/src/assets/icons/file-types/crystal_light.svg
create mode 100644 ui/src/assets/icons/file-types/csharp.svg
create mode 100644 ui/src/assets/icons/file-types/css-map.svg
create mode 100644 ui/src/assets/icons/file-types/css.svg
create mode 100644 ui/src/assets/icons/file-types/cucumber.svg
create mode 100644 ui/src/assets/icons/file-types/cuda.svg
create mode 100644 ui/src/assets/icons/file-types/cursor.svg
create mode 100644 ui/src/assets/icons/file-types/cursor_light.svg
create mode 100644 ui/src/assets/icons/file-types/cypress.svg
create mode 100644 ui/src/assets/icons/file-types/d.svg
create mode 100644 ui/src/assets/icons/file-types/dart.svg
create mode 100644 ui/src/assets/icons/file-types/dart_generated.svg
create mode 100644 ui/src/assets/icons/file-types/database.svg
create mode 100644 ui/src/assets/icons/file-types/deepsource.svg
create mode 100644 ui/src/assets/icons/file-types/denizenscript.svg
create mode 100644 ui/src/assets/icons/file-types/deno.svg
create mode 100644 ui/src/assets/icons/file-types/deno_light.svg
create mode 100644 ui/src/assets/icons/file-types/dependabot.svg
create mode 100644 ui/src/assets/icons/file-types/dependencies-update.svg
create mode 100644 ui/src/assets/icons/file-types/dhall.svg
create mode 100644 ui/src/assets/icons/file-types/diff.svg
create mode 100644 ui/src/assets/icons/file-types/dinophp.svg
create mode 100644 ui/src/assets/icons/file-types/disc.svg
create mode 100644 ui/src/assets/icons/file-types/django.svg
create mode 100644 ui/src/assets/icons/file-types/dll.svg
create mode 100644 ui/src/assets/icons/file-types/docker.svg
create mode 100644 ui/src/assets/icons/file-types/doctex-installer.svg
create mode 100644 ui/src/assets/icons/file-types/document.svg
create mode 100644 ui/src/assets/icons/file-types/dotjs.svg
create mode 100644 ui/src/assets/icons/file-types/drawio.svg
create mode 100644 ui/src/assets/icons/file-types/drizzle.svg
create mode 100644 ui/src/assets/icons/file-types/drone.svg
create mode 100644 ui/src/assets/icons/file-types/drone_light.svg
create mode 100644 ui/src/assets/icons/file-types/duc.svg
create mode 100644 ui/src/assets/icons/file-types/dune.svg
create mode 100644 ui/src/assets/icons/file-types/edge.svg
create mode 100644 ui/src/assets/icons/file-types/editorconfig.svg
create mode 100644 ui/src/assets/icons/file-types/ejs.svg
create mode 100644 ui/src/assets/icons/file-types/elixir.svg
create mode 100644 ui/src/assets/icons/file-types/elm.svg
create mode 100644 ui/src/assets/icons/file-types/email.svg
create mode 100644 ui/src/assets/icons/file-types/ember.svg
create mode 100644 ui/src/assets/icons/file-types/epub.svg
create mode 100644 ui/src/assets/icons/file-types/erlang.svg
create mode 100644 ui/src/assets/icons/file-types/esbuild.svg
create mode 100644 ui/src/assets/icons/file-types/eslint.svg
create mode 100644 ui/src/assets/icons/file-types/excalidraw.svg
create mode 100644 ui/src/assets/icons/file-types/exe.svg
create mode 100644 ui/src/assets/icons/file-types/fastlane.svg
create mode 100644 ui/src/assets/icons/file-types/favicon.svg
create mode 100644 ui/src/assets/icons/file-types/figma.svg
create mode 100644 ui/src/assets/icons/file-types/firebase.svg
create mode 100644 ui/src/assets/icons/file-types/flash.svg
create mode 100644 ui/src/assets/icons/file-types/flow.svg
create mode 100644 ui/src/assets/icons/file-types/folder-admin-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-admin.svg
create mode 100644 ui/src/assets/icons/file-types/folder-android-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-android.svg
create mode 100644 ui/src/assets/icons/file-types/folder-angular-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-angular.svg
create mode 100644 ui/src/assets/icons/file-types/folder-animation-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-animation.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ansible-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ansible.svg
create mode 100644 ui/src/assets/icons/file-types/folder-api-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-api.svg
create mode 100644 ui/src/assets/icons/file-types/folder-apollo-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-apollo.svg
create mode 100644 ui/src/assets/icons/file-types/folder-app-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-app.svg
create mode 100644 ui/src/assets/icons/file-types/folder-archive-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-archive.svg
create mode 100644 ui/src/assets/icons/file-types/folder-astro-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-astro.svg
create mode 100644 ui/src/assets/icons/file-types/folder-atom-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-atom.svg
create mode 100644 ui/src/assets/icons/file-types/folder-attachment-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-attachment.svg
create mode 100644 ui/src/assets/icons/file-types/folder-audio-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-audio.svg
create mode 100644 ui/src/assets/icons/file-types/folder-aurelia-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-aurelia.svg
create mode 100644 ui/src/assets/icons/file-types/folder-aws-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-aws.svg
create mode 100644 ui/src/assets/icons/file-types/folder-azure-pipelines-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-azure-pipelines.svg
create mode 100644 ui/src/assets/icons/file-types/folder-backup-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-backup.svg
create mode 100644 ui/src/assets/icons/file-types/folder-base-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-base.svg
create mode 100644 ui/src/assets/icons/file-types/folder-batch-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-batch.svg
create mode 100644 ui/src/assets/icons/file-types/folder-benchmark-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-benchmark.svg
create mode 100644 ui/src/assets/icons/file-types/folder-bibliography-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-bibliography.svg
create mode 100644 ui/src/assets/icons/file-types/folder-bicep-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-bicep.svg
create mode 100644 ui/src/assets/icons/file-types/folder-blender-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-blender.svg
create mode 100644 ui/src/assets/icons/file-types/folder-bloc-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-bloc.svg
create mode 100644 ui/src/assets/icons/file-types/folder-bower-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-bower.svg
create mode 100644 ui/src/assets/icons/file-types/folder-buildkite-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-buildkite.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cart-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cart.svg
create mode 100644 ui/src/assets/icons/file-types/folder-changesets-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-changesets.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ci-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ci.svg
create mode 100644 ui/src/assets/icons/file-types/folder-circleci-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-circleci.svg
create mode 100644 ui/src/assets/icons/file-types/folder-class-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-class.svg
create mode 100644 ui/src/assets/icons/file-types/folder-claude-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-claude.svg
create mode 100644 ui/src/assets/icons/file-types/folder-client-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-client.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cline-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cline.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cloud-functions-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cloud-functions.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cloudflare-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cloudflare.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cluster-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cluster.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cobol-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cobol.svg
create mode 100644 ui/src/assets/icons/file-types/folder-command-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-command.svg
create mode 100644 ui/src/assets/icons/file-types/folder-components-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-components.svg
create mode 100644 ui/src/assets/icons/file-types/folder-config-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-config.svg
create mode 100644 ui/src/assets/icons/file-types/folder-connection-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-connection.svg
create mode 100644 ui/src/assets/icons/file-types/folder-console-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-console.svg
create mode 100644 ui/src/assets/icons/file-types/folder-constant-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-constant.svg
create mode 100644 ui/src/assets/icons/file-types/folder-container-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-container.svg
create mode 100644 ui/src/assets/icons/file-types/folder-content-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-content.svg
create mode 100644 ui/src/assets/icons/file-types/folder-context-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-context.svg
create mode 100644 ui/src/assets/icons/file-types/folder-contract-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-contract.svg
create mode 100644 ui/src/assets/icons/file-types/folder-controller-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-controller.svg
create mode 100644 ui/src/assets/icons/file-types/folder-core-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-core.svg
create mode 100644 ui/src/assets/icons/file-types/folder-coverage-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-coverage.svg
create mode 100644 ui/src/assets/icons/file-types/folder-css-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-css.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cursor-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cursor-open_light.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cursor.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cursor_light.svg
create mode 100644 ui/src/assets/icons/file-types/folder-custom-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-custom.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cypress-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-cypress.svg
create mode 100644 ui/src/assets/icons/file-types/folder-dart-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-dart.svg
create mode 100644 ui/src/assets/icons/file-types/folder-database-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-database.svg
create mode 100644 ui/src/assets/icons/file-types/folder-debug-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-debug.svg
create mode 100644 ui/src/assets/icons/file-types/folder-decorators-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-decorators.svg
create mode 100644 ui/src/assets/icons/file-types/folder-delta-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-delta.svg
create mode 100644 ui/src/assets/icons/file-types/folder-desktop-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-desktop.svg
create mode 100644 ui/src/assets/icons/file-types/folder-directive-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-directive.svg
create mode 100644 ui/src/assets/icons/file-types/folder-dist-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-dist.svg
create mode 100644 ui/src/assets/icons/file-types/folder-docker-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-docker.svg
create mode 100644 ui/src/assets/icons/file-types/folder-docs-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-docs.svg
create mode 100644 ui/src/assets/icons/file-types/folder-download-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-download.svg
create mode 100644 ui/src/assets/icons/file-types/folder-drizzle-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-drizzle.svg
create mode 100644 ui/src/assets/icons/file-types/folder-dump-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-dump.svg
create mode 100644 ui/src/assets/icons/file-types/folder-element-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-element.svg
create mode 100644 ui/src/assets/icons/file-types/folder-enum-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-enum.svg
create mode 100644 ui/src/assets/icons/file-types/folder-environment-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-environment.svg
create mode 100644 ui/src/assets/icons/file-types/folder-error-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-error.svg
create mode 100644 ui/src/assets/icons/file-types/folder-event-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-event.svg
create mode 100644 ui/src/assets/icons/file-types/folder-examples-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-examples.svg
create mode 100644 ui/src/assets/icons/file-types/folder-expo-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-expo.svg
create mode 100644 ui/src/assets/icons/file-types/folder-export-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-export.svg
create mode 100644 ui/src/assets/icons/file-types/folder-fastlane-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-fastlane.svg
create mode 100644 ui/src/assets/icons/file-types/folder-favicon-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-favicon.svg
create mode 100644 ui/src/assets/icons/file-types/folder-firebase-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-firebase.svg
create mode 100644 ui/src/assets/icons/file-types/folder-firestore-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-firestore.svg
create mode 100644 ui/src/assets/icons/file-types/folder-flow-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-flow.svg
create mode 100644 ui/src/assets/icons/file-types/folder-flutter-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-flutter.svg
create mode 100644 ui/src/assets/icons/file-types/folder-font-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-font.svg
create mode 100644 ui/src/assets/icons/file-types/folder-forgejo-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-forgejo.svg
create mode 100644 ui/src/assets/icons/file-types/folder-functions-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-functions.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gamemaker-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gamemaker.svg
create mode 100644 ui/src/assets/icons/file-types/folder-generator-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-generator.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gh-workflows-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gh-workflows.svg
create mode 100644 ui/src/assets/icons/file-types/folder-git-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-git.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gitea-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gitea.svg
create mode 100644 ui/src/assets/icons/file-types/folder-github-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-github.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gitlab-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gitlab.svg
create mode 100644 ui/src/assets/icons/file-types/folder-global-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-global.svg
create mode 100644 ui/src/assets/icons/file-types/folder-godot-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-godot.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gradle-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gradle.svg
create mode 100644 ui/src/assets/icons/file-types/folder-graphql-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-graphql.svg
create mode 100644 ui/src/assets/icons/file-types/folder-guard-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-guard.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gulp-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-gulp.svg
create mode 100644 ui/src/assets/icons/file-types/folder-helm-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-helm.svg
create mode 100644 ui/src/assets/icons/file-types/folder-helper-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-helper.svg
create mode 100644 ui/src/assets/icons/file-types/folder-home-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-home.svg
create mode 100644 ui/src/assets/icons/file-types/folder-hook-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-hook.svg
create mode 100644 ui/src/assets/icons/file-types/folder-husky-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-husky.svg
create mode 100644 ui/src/assets/icons/file-types/folder-i18n-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-i18n.svg
create mode 100644 ui/src/assets/icons/file-types/folder-images-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-images.svg
create mode 100644 ui/src/assets/icons/file-types/folder-import-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-import.svg
create mode 100644 ui/src/assets/icons/file-types/folder-include-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-include.svg
create mode 100644 ui/src/assets/icons/file-types/folder-intellij-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-intellij-open_light.svg
create mode 100644 ui/src/assets/icons/file-types/folder-intellij.svg
create mode 100644 ui/src/assets/icons/file-types/folder-intellij_light.svg
create mode 100644 ui/src/assets/icons/file-types/folder-interceptor-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-interceptor.svg
create mode 100644 ui/src/assets/icons/file-types/folder-interface-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-interface.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ios-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ios.svg
create mode 100644 ui/src/assets/icons/file-types/folder-java-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-java.svg
create mode 100644 ui/src/assets/icons/file-types/folder-javascript-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-javascript.svg
create mode 100644 ui/src/assets/icons/file-types/folder-jinja-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-jinja-open_light.svg
create mode 100644 ui/src/assets/icons/file-types/folder-jinja.svg
create mode 100644 ui/src/assets/icons/file-types/folder-jinja_light.svg
create mode 100644 ui/src/assets/icons/file-types/folder-job-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-job.svg
create mode 100644 ui/src/assets/icons/file-types/folder-json-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-json.svg
create mode 100644 ui/src/assets/icons/file-types/folder-jupyter-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-jupyter.svg
create mode 100644 ui/src/assets/icons/file-types/folder-keys-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-keys.svg
create mode 100644 ui/src/assets/icons/file-types/folder-kubernetes-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-kubernetes.svg
create mode 100644 ui/src/assets/icons/file-types/folder-kusto-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-kusto.svg
create mode 100644 ui/src/assets/icons/file-types/folder-layout-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-layout.svg
create mode 100644 ui/src/assets/icons/file-types/folder-lefthook-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-lefthook.svg
create mode 100644 ui/src/assets/icons/file-types/folder-less-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-less.svg
create mode 100644 ui/src/assets/icons/file-types/folder-lib-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-lib.svg
create mode 100644 ui/src/assets/icons/file-types/folder-link-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-link.svg
create mode 100644 ui/src/assets/icons/file-types/folder-linux-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-linux.svg
create mode 100644 ui/src/assets/icons/file-types/folder-liquibase-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-liquibase.svg
create mode 100644 ui/src/assets/icons/file-types/folder-log-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-log.svg
create mode 100644 ui/src/assets/icons/file-types/folder-lottie-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-lottie.svg
create mode 100644 ui/src/assets/icons/file-types/folder-lua-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-lua.svg
create mode 100644 ui/src/assets/icons/file-types/folder-luau-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-luau.svg
create mode 100644 ui/src/assets/icons/file-types/folder-macos-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-macos.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mail-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mail.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mappings-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mappings.svg
create mode 100644 ui/src/assets/icons/file-types/folder-markdown-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-markdown.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mercurial-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mercurial.svg
create mode 100644 ui/src/assets/icons/file-types/folder-messages-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-messages.svg
create mode 100644 ui/src/assets/icons/file-types/folder-meta-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-meta.svg
create mode 100644 ui/src/assets/icons/file-types/folder-middleware-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-middleware.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mjml-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mjml.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mobile-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mobile.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mock-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mock.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mojo-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-mojo.svg
create mode 100644 ui/src/assets/icons/file-types/folder-molecule-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-molecule.svg
create mode 100644 ui/src/assets/icons/file-types/folder-moon-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-moon.svg
create mode 100644 ui/src/assets/icons/file-types/folder-netlify-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-netlify.svg
create mode 100644 ui/src/assets/icons/file-types/folder-next-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-next.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ngrx-store-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ngrx-store.svg
create mode 100644 ui/src/assets/icons/file-types/folder-node-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-node.svg
create mode 100644 ui/src/assets/icons/file-types/folder-nuxt-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-nuxt.svg
create mode 100644 ui/src/assets/icons/file-types/folder-obsidian-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-obsidian.svg
create mode 100644 ui/src/assets/icons/file-types/folder-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-organism-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-organism.svg
create mode 100644 ui/src/assets/icons/file-types/folder-other-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-other.svg
create mode 100644 ui/src/assets/icons/file-types/folder-packages-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-packages.svg
create mode 100644 ui/src/assets/icons/file-types/folder-pdf-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-pdf.svg
create mode 100644 ui/src/assets/icons/file-types/folder-pdm-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-pdm.svg
create mode 100644 ui/src/assets/icons/file-types/folder-php-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-php.svg
create mode 100644 ui/src/assets/icons/file-types/folder-phpmailer-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-phpmailer.svg
create mode 100644 ui/src/assets/icons/file-types/folder-pipe-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-pipe.svg
create mode 100644 ui/src/assets/icons/file-types/folder-plastic-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-plastic.svg
create mode 100644 ui/src/assets/icons/file-types/folder-plugin-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-plugin.svg
create mode 100644 ui/src/assets/icons/file-types/folder-policy-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-policy.svg
create mode 100644 ui/src/assets/icons/file-types/folder-powershell-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-powershell.svg
create mode 100644 ui/src/assets/icons/file-types/folder-prisma-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-prisma.svg
create mode 100644 ui/src/assets/icons/file-types/folder-private-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-private.svg
create mode 100644 ui/src/assets/icons/file-types/folder-project-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-project.svg
create mode 100644 ui/src/assets/icons/file-types/folder-prompts-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-prompts.svg
create mode 100644 ui/src/assets/icons/file-types/folder-proto-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-proto.svg
create mode 100644 ui/src/assets/icons/file-types/folder-public-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-public.svg
create mode 100644 ui/src/assets/icons/file-types/folder-python-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-python.svg
create mode 100644 ui/src/assets/icons/file-types/folder-pytorch-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-pytorch.svg
create mode 100644 ui/src/assets/icons/file-types/folder-quasar-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-quasar.svg
create mode 100644 ui/src/assets/icons/file-types/folder-queue-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-queue.svg
create mode 100644 ui/src/assets/icons/file-types/folder-react-components-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-react-components.svg
create mode 100644 ui/src/assets/icons/file-types/folder-redux-reducer-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-redux-reducer.svg
create mode 100644 ui/src/assets/icons/file-types/folder-repository-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-repository.svg
create mode 100644 ui/src/assets/icons/file-types/folder-resolver-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-resolver.svg
create mode 100644 ui/src/assets/icons/file-types/folder-resource-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-resource.svg
create mode 100644 ui/src/assets/icons/file-types/folder-review-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-review.svg
create mode 100644 ui/src/assets/icons/file-types/folder-robot-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-robot.svg
create mode 100644 ui/src/assets/icons/file-types/folder-routes-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-routes.svg
create mode 100644 ui/src/assets/icons/file-types/folder-rules-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-rules.svg
create mode 100644 ui/src/assets/icons/file-types/folder-rust-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-rust.svg
create mode 100644 ui/src/assets/icons/file-types/folder-sandbox-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-sandbox.svg
create mode 100644 ui/src/assets/icons/file-types/folder-sass-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-sass.svg
create mode 100644 ui/src/assets/icons/file-types/folder-scala-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-scala.svg
create mode 100644 ui/src/assets/icons/file-types/folder-scons-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-scons.svg
create mode 100644 ui/src/assets/icons/file-types/folder-scripts-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-scripts.svg
create mode 100644 ui/src/assets/icons/file-types/folder-secure-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-secure.svg
create mode 100644 ui/src/assets/icons/file-types/folder-seeders-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-seeders.svg
create mode 100644 ui/src/assets/icons/file-types/folder-server-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-server.svg
create mode 100644 ui/src/assets/icons/file-types/folder-serverless-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-serverless.svg
create mode 100644 ui/src/assets/icons/file-types/folder-shader-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-shader.svg
create mode 100644 ui/src/assets/icons/file-types/folder-shared-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-shared.svg
create mode 100644 ui/src/assets/icons/file-types/folder-snapcraft-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-snapcraft.svg
create mode 100644 ui/src/assets/icons/file-types/folder-snippet-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-snippet.svg
create mode 100644 ui/src/assets/icons/file-types/folder-src-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-src-tauri-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-src-tauri.svg
create mode 100644 ui/src/assets/icons/file-types/folder-src.svg
create mode 100644 ui/src/assets/icons/file-types/folder-stack-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-stack.svg
create mode 100644 ui/src/assets/icons/file-types/folder-stencil-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-stencil.svg
create mode 100644 ui/src/assets/icons/file-types/folder-store-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-store.svg
create mode 100644 ui/src/assets/icons/file-types/folder-storybook-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-storybook.svg
create mode 100644 ui/src/assets/icons/file-types/folder-stylus-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-stylus.svg
create mode 100644 ui/src/assets/icons/file-types/folder-sublime-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-sublime.svg
create mode 100644 ui/src/assets/icons/file-types/folder-supabase-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-supabase.svg
create mode 100644 ui/src/assets/icons/file-types/folder-svelte-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-svelte.svg
create mode 100644 ui/src/assets/icons/file-types/folder-svg-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-svg.svg
create mode 100644 ui/src/assets/icons/file-types/folder-syntax-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-syntax.svg
create mode 100644 ui/src/assets/icons/file-types/folder-target-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-target.svg
create mode 100644 ui/src/assets/icons/file-types/folder-taskfile-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-taskfile.svg
create mode 100644 ui/src/assets/icons/file-types/folder-tasks-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-tasks.svg
create mode 100644 ui/src/assets/icons/file-types/folder-television-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-television.svg
create mode 100644 ui/src/assets/icons/file-types/folder-temp-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-temp.svg
create mode 100644 ui/src/assets/icons/file-types/folder-template-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-template.svg
create mode 100644 ui/src/assets/icons/file-types/folder-terraform-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-terraform.svg
create mode 100644 ui/src/assets/icons/file-types/folder-test-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-test.svg
create mode 100644 ui/src/assets/icons/file-types/folder-theme-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-theme.svg
create mode 100644 ui/src/assets/icons/file-types/folder-tools-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-tools.svg
create mode 100644 ui/src/assets/icons/file-types/folder-trash-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-trash.svg
create mode 100644 ui/src/assets/icons/file-types/folder-trigger-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-trigger.svg
create mode 100644 ui/src/assets/icons/file-types/folder-turborepo-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-turborepo.svg
create mode 100644 ui/src/assets/icons/file-types/folder-typescript-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-typescript.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ui-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-ui.svg
create mode 100644 ui/src/assets/icons/file-types/folder-unity-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-unity.svg
create mode 100644 ui/src/assets/icons/file-types/folder-update-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-update.svg
create mode 100644 ui/src/assets/icons/file-types/folder-upload-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-upload.svg
create mode 100644 ui/src/assets/icons/file-types/folder-utils-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-utils.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vercel-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vercel.svg
create mode 100644 ui/src/assets/icons/file-types/folder-verdaccio-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-verdaccio.svg
create mode 100644 ui/src/assets/icons/file-types/folder-video-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-video.svg
create mode 100644 ui/src/assets/icons/file-types/folder-views-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-views.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vm-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vm.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vscode-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vscode.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vue-directives-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vue-directives.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vue-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vue.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vuepress-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vuepress.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vuex-store-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-vuex-store.svg
create mode 100644 ui/src/assets/icons/file-types/folder-wakatime-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-wakatime.svg
create mode 100644 ui/src/assets/icons/file-types/folder-webpack-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-webpack.svg
create mode 100644 ui/src/assets/icons/file-types/folder-windows-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-windows.svg
create mode 100644 ui/src/assets/icons/file-types/folder-wordpress-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-wordpress.svg
create mode 100644 ui/src/assets/icons/file-types/folder-yarn-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-yarn.svg
create mode 100644 ui/src/assets/icons/file-types/folder-zeabur-open.svg
create mode 100644 ui/src/assets/icons/file-types/folder-zeabur.svg
create mode 100644 ui/src/assets/icons/file-types/folder.svg
create mode 100644 ui/src/assets/icons/file-types/font.svg
create mode 100644 ui/src/assets/icons/file-types/forth.svg
create mode 100644 ui/src/assets/icons/file-types/fortran.svg
create mode 100644 ui/src/assets/icons/file-types/foxpro.svg
create mode 100644 ui/src/assets/icons/file-types/freemarker.svg
create mode 100644 ui/src/assets/icons/file-types/fsharp.svg
create mode 100644 ui/src/assets/icons/file-types/fusebox.svg
create mode 100644 ui/src/assets/icons/file-types/gamemaker.svg
create mode 100644 ui/src/assets/icons/file-types/garden.svg
create mode 100644 ui/src/assets/icons/file-types/gatsby.svg
create mode 100644 ui/src/assets/icons/file-types/gcp.svg
create mode 100644 ui/src/assets/icons/file-types/gemfile.svg
create mode 100644 ui/src/assets/icons/file-types/gemini-ai.svg
create mode 100644 ui/src/assets/icons/file-types/gemini.svg
create mode 100644 ui/src/assets/icons/file-types/git.svg
create mode 100644 ui/src/assets/icons/file-types/github-actions-workflow.svg
create mode 100644 ui/src/assets/icons/file-types/github-sponsors.svg
create mode 100644 ui/src/assets/icons/file-types/gitlab.svg
create mode 100644 ui/src/assets/icons/file-types/gitpod.svg
create mode 100644 ui/src/assets/icons/file-types/gleam.svg
create mode 100644 ui/src/assets/icons/file-types/gnuplot.svg
create mode 100644 ui/src/assets/icons/file-types/go-mod.svg
create mode 100644 ui/src/assets/icons/file-types/go.svg
create mode 100644 ui/src/assets/icons/file-types/go_gopher.svg
create mode 100644 ui/src/assets/icons/file-types/godot-assets.svg
create mode 100644 ui/src/assets/icons/file-types/godot.svg
create mode 100644 ui/src/assets/icons/file-types/gradle.svg
create mode 100644 ui/src/assets/icons/file-types/grafana-alloy.svg
create mode 100644 ui/src/assets/icons/file-types/grain.svg
create mode 100644 ui/src/assets/icons/file-types/graphcool.svg
create mode 100644 ui/src/assets/icons/file-types/graphql.svg
create mode 100644 ui/src/assets/icons/file-types/gridsome.svg
create mode 100644 ui/src/assets/icons/file-types/groovy.svg
create mode 100644 ui/src/assets/icons/file-types/grunt.svg
create mode 100644 ui/src/assets/icons/file-types/gulp.svg
create mode 100644 ui/src/assets/icons/file-types/h.svg
create mode 100644 ui/src/assets/icons/file-types/hack.svg
create mode 100644 ui/src/assets/icons/file-types/hadolint.svg
create mode 100644 ui/src/assets/icons/file-types/haml.svg
create mode 100644 ui/src/assets/icons/file-types/handlebars.svg
create mode 100644 ui/src/assets/icons/file-types/hardhat.svg
create mode 100644 ui/src/assets/icons/file-types/harmonix.svg
create mode 100644 ui/src/assets/icons/file-types/haskell.svg
create mode 100644 ui/src/assets/icons/file-types/haxe.svg
create mode 100644 ui/src/assets/icons/file-types/hcl.svg
create mode 100644 ui/src/assets/icons/file-types/hcl_light.svg
create mode 100644 ui/src/assets/icons/file-types/helm.svg
create mode 100644 ui/src/assets/icons/file-types/heroku.svg
create mode 100644 ui/src/assets/icons/file-types/hex.svg
create mode 100644 ui/src/assets/icons/file-types/histoire.svg
create mode 100644 ui/src/assets/icons/file-types/hjson.svg
create mode 100644 ui/src/assets/icons/file-types/horusec.svg
create mode 100644 ui/src/assets/icons/file-types/hosts.svg
create mode 100644 ui/src/assets/icons/file-types/hosts_light.svg
create mode 100644 ui/src/assets/icons/file-types/hpp.svg
create mode 100644 ui/src/assets/icons/file-types/html.svg
create mode 100644 ui/src/assets/icons/file-types/http.svg
create mode 100644 ui/src/assets/icons/file-types/huff.svg
create mode 100644 ui/src/assets/icons/file-types/huff_light.svg
create mode 100644 ui/src/assets/icons/file-types/hurl.svg
create mode 100644 ui/src/assets/icons/file-types/husky.svg
create mode 100644 ui/src/assets/icons/file-types/i18n.svg
create mode 100644 ui/src/assets/icons/file-types/idris.svg
create mode 100644 ui/src/assets/icons/file-types/ifanr-cloud.svg
create mode 100644 ui/src/assets/icons/file-types/image.svg
create mode 100644 ui/src/assets/icons/file-types/imba.svg
create mode 100644 ui/src/assets/icons/file-types/installation.svg
create mode 100644 ui/src/assets/icons/file-types/ionic.svg
create mode 100644 ui/src/assets/icons/file-types/istanbul.svg
create mode 100644 ui/src/assets/icons/file-types/jar.svg
create mode 100644 ui/src/assets/icons/file-types/java.svg
create mode 100644 ui/src/assets/icons/file-types/javaclass.svg
create mode 100644 ui/src/assets/icons/file-types/javascript-map.svg
create mode 100644 ui/src/assets/icons/file-types/javascript.svg
create mode 100644 ui/src/assets/icons/file-types/jenkins.svg
create mode 100644 ui/src/assets/icons/file-types/jest.svg
create mode 100644 ui/src/assets/icons/file-types/jinja.svg
create mode 100644 ui/src/assets/icons/file-types/jinja_light.svg
create mode 100644 ui/src/assets/icons/file-types/jsconfig.svg
create mode 100644 ui/src/assets/icons/file-types/json.svg
create mode 100644 ui/src/assets/icons/file-types/jsr.svg
create mode 100644 ui/src/assets/icons/file-types/jsr_light.svg
create mode 100644 ui/src/assets/icons/file-types/julia.svg
create mode 100644 ui/src/assets/icons/file-types/jupyter.svg
create mode 100644 ui/src/assets/icons/file-types/just.svg
create mode 100644 ui/src/assets/icons/file-types/karma.svg
create mode 100644 ui/src/assets/icons/file-types/kcl.svg
create mode 100644 ui/src/assets/icons/file-types/key.svg
create mode 100644 ui/src/assets/icons/file-types/keystatic.svg
create mode 100644 ui/src/assets/icons/file-types/kivy.svg
create mode 100644 ui/src/assets/icons/file-types/kl.svg
create mode 100644 ui/src/assets/icons/file-types/knip.svg
create mode 100644 ui/src/assets/icons/file-types/kotlin.svg
create mode 100644 ui/src/assets/icons/file-types/kubernetes.svg
create mode 100644 ui/src/assets/icons/file-types/kusto.svg
create mode 100644 ui/src/assets/icons/file-types/label.svg
create mode 100644 ui/src/assets/icons/file-types/laravel.svg
create mode 100644 ui/src/assets/icons/file-types/latexmk.svg
create mode 100644 ui/src/assets/icons/file-types/lbx.svg
create mode 100644 ui/src/assets/icons/file-types/lefthook.svg
create mode 100644 ui/src/assets/icons/file-types/lerna.svg
create mode 100644 ui/src/assets/icons/file-types/less.svg
create mode 100644 ui/src/assets/icons/file-types/liara.svg
create mode 100644 ui/src/assets/icons/file-types/lib.svg
create mode 100644 ui/src/assets/icons/file-types/lighthouse.svg
create mode 100644 ui/src/assets/icons/file-types/lilypond.svg
create mode 100644 ui/src/assets/icons/file-types/lintstaged.svg
create mode 100644 ui/src/assets/icons/file-types/liquid.svg
create mode 100644 ui/src/assets/icons/file-types/lisp.svg
create mode 100644 ui/src/assets/icons/file-types/livescript.svg
create mode 100644 ui/src/assets/icons/file-types/lock.svg
create mode 100644 ui/src/assets/icons/file-types/log.svg
create mode 100644 ui/src/assets/icons/file-types/lolcode.svg
create mode 100644 ui/src/assets/icons/file-types/lottie.svg
create mode 100644 ui/src/assets/icons/file-types/lua.svg
create mode 100644 ui/src/assets/icons/file-types/luau.svg
create mode 100644 ui/src/assets/icons/file-types/lyric.svg
create mode 100644 ui/src/assets/icons/file-types/makefile.svg
create mode 100644 ui/src/assets/icons/file-types/markdoc-config.svg
create mode 100644 ui/src/assets/icons/file-types/markdoc.svg
create mode 100644 ui/src/assets/icons/file-types/markdown.svg
create mode 100644 ui/src/assets/icons/file-types/markdownlint.svg
create mode 100644 ui/src/assets/icons/file-types/markojs.svg
create mode 100644 ui/src/assets/icons/file-types/mathematica.svg
create mode 100644 ui/src/assets/icons/file-types/matlab.svg
create mode 100644 ui/src/assets/icons/file-types/maven.svg
create mode 100644 ui/src/assets/icons/file-types/mdsvex.svg
create mode 100644 ui/src/assets/icons/file-types/mdx.svg
create mode 100644 ui/src/assets/icons/file-types/mercurial.svg
create mode 100644 ui/src/assets/icons/file-types/merlin.svg
create mode 100644 ui/src/assets/icons/file-types/mermaid.svg
create mode 100644 ui/src/assets/icons/file-types/meson.svg
create mode 100644 ui/src/assets/icons/file-types/minecraft-fabric.svg
create mode 100644 ui/src/assets/icons/file-types/minecraft.svg
create mode 100644 ui/src/assets/icons/file-types/mint.svg
create mode 100644 ui/src/assets/icons/file-types/mjml.svg
create mode 100644 ui/src/assets/icons/file-types/mocha.svg
create mode 100644 ui/src/assets/icons/file-types/modernizr.svg
create mode 100644 ui/src/assets/icons/file-types/mojo.svg
create mode 100644 ui/src/assets/icons/file-types/moon.svg
create mode 100644 ui/src/assets/icons/file-types/moonscript.svg
create mode 100644 ui/src/assets/icons/file-types/mxml.svg
create mode 100644 ui/src/assets/icons/file-types/nano-staged.svg
create mode 100644 ui/src/assets/icons/file-types/nano-staged_light.svg
create mode 100644 ui/src/assets/icons/file-types/ndst.svg
create mode 100644 ui/src/assets/icons/file-types/nest.svg
create mode 100644 ui/src/assets/icons/file-types/netlify.svg
create mode 100644 ui/src/assets/icons/file-types/netlify_light.svg
create mode 100644 ui/src/assets/icons/file-types/next.svg
create mode 100644 ui/src/assets/icons/file-types/next_light.svg
create mode 100644 ui/src/assets/icons/file-types/nginx.svg
create mode 100644 ui/src/assets/icons/file-types/ngrx-actions.svg
create mode 100644 ui/src/assets/icons/file-types/ngrx-effects.svg
create mode 100644 ui/src/assets/icons/file-types/ngrx-entity.svg
create mode 100644 ui/src/assets/icons/file-types/ngrx-reducer.svg
create mode 100644 ui/src/assets/icons/file-types/ngrx-selectors.svg
create mode 100644 ui/src/assets/icons/file-types/ngrx-state.svg
create mode 100644 ui/src/assets/icons/file-types/nim.svg
create mode 100644 ui/src/assets/icons/file-types/nix.svg
create mode 100644 ui/src/assets/icons/file-types/nodejs.svg
create mode 100644 ui/src/assets/icons/file-types/nodejs_alt.svg
create mode 100644 ui/src/assets/icons/file-types/nodemon.svg
create mode 100644 ui/src/assets/icons/file-types/npm.svg
create mode 100644 ui/src/assets/icons/file-types/nuget.svg
create mode 100644 ui/src/assets/icons/file-types/nunjucks.svg
create mode 100644 ui/src/assets/icons/file-types/nuxt.svg
create mode 100644 ui/src/assets/icons/file-types/nx.svg
create mode 100644 ui/src/assets/icons/file-types/objective-c.svg
create mode 100644 ui/src/assets/icons/file-types/objective-cpp.svg
create mode 100644 ui/src/assets/icons/file-types/ocaml.svg
create mode 100644 ui/src/assets/icons/file-types/odin.svg
create mode 100644 ui/src/assets/icons/file-types/opa.svg
create mode 100644 ui/src/assets/icons/file-types/opam.svg
create mode 100644 ui/src/assets/icons/file-types/openapi.svg
create mode 100644 ui/src/assets/icons/file-types/openapi_light.svg
create mode 100644 ui/src/assets/icons/file-types/otne.svg
create mode 100644 ui/src/assets/icons/file-types/oxlint.svg
create mode 100644 ui/src/assets/icons/file-types/packship.svg
create mode 100644 ui/src/assets/icons/file-types/palette.svg
create mode 100644 ui/src/assets/icons/file-types/panda.svg
create mode 100644 ui/src/assets/icons/file-types/parcel.svg
create mode 100644 ui/src/assets/icons/file-types/pascal.svg
create mode 100644 ui/src/assets/icons/file-types/pawn.svg
create mode 100644 ui/src/assets/icons/file-types/payload.svg
create mode 100644 ui/src/assets/icons/file-types/payload_light.svg
create mode 100644 ui/src/assets/icons/file-types/pdf.svg
create mode 100644 ui/src/assets/icons/file-types/pdm.svg
create mode 100644 ui/src/assets/icons/file-types/percy.svg
create mode 100644 ui/src/assets/icons/file-types/perl.svg
create mode 100644 ui/src/assets/icons/file-types/php-cs-fixer.svg
create mode 100644 ui/src/assets/icons/file-types/php.svg
create mode 100644 ui/src/assets/icons/file-types/php_elephant.svg
create mode 100644 ui/src/assets/icons/file-types/php_elephant_pink.svg
create mode 100644 ui/src/assets/icons/file-types/phpstan.svg
create mode 100644 ui/src/assets/icons/file-types/phpunit.svg
create mode 100644 ui/src/assets/icons/file-types/pinejs.svg
create mode 100644 ui/src/assets/icons/file-types/pipeline.svg
create mode 100644 ui/src/assets/icons/file-types/pkl.svg
create mode 100644 ui/src/assets/icons/file-types/plastic.svg
create mode 100644 ui/src/assets/icons/file-types/playwright.svg
create mode 100644 ui/src/assets/icons/file-types/plop.svg
create mode 100644 ui/src/assets/icons/file-types/pm2-ecosystem.svg
create mode 100644 ui/src/assets/icons/file-types/pnpm.svg
create mode 100644 ui/src/assets/icons/file-types/pnpm_light.svg
create mode 100644 ui/src/assets/icons/file-types/poetry.svg
create mode 100644 ui/src/assets/icons/file-types/postcss.svg
create mode 100644 ui/src/assets/icons/file-types/posthtml.svg
create mode 100644 ui/src/assets/icons/file-types/powerpoint.svg
create mode 100644 ui/src/assets/icons/file-types/powershell.svg
create mode 100644 ui/src/assets/icons/file-types/pre-commit.svg
create mode 100644 ui/src/assets/icons/file-types/prettier.svg
create mode 100644 ui/src/assets/icons/file-types/prisma.svg
create mode 100644 ui/src/assets/icons/file-types/processing.svg
create mode 100644 ui/src/assets/icons/file-types/prolog.svg
create mode 100644 ui/src/assets/icons/file-types/prompt.svg
create mode 100644 ui/src/assets/icons/file-types/proto.svg
create mode 100644 ui/src/assets/icons/file-types/protractor.svg
create mode 100644 ui/src/assets/icons/file-types/pug.svg
create mode 100644 ui/src/assets/icons/file-types/puppet.svg
create mode 100644 ui/src/assets/icons/file-types/puppeteer.svg
create mode 100644 ui/src/assets/icons/file-types/purescript.svg
create mode 100644 ui/src/assets/icons/file-types/python-misc.svg
create mode 100644 ui/src/assets/icons/file-types/python.svg
create mode 100644 ui/src/assets/icons/file-types/pytorch.svg
create mode 100644 ui/src/assets/icons/file-types/qsharp.svg
create mode 100644 ui/src/assets/icons/file-types/quarto.svg
create mode 100644 ui/src/assets/icons/file-types/quasar.svg
create mode 100644 ui/src/assets/icons/file-types/quokka.svg
create mode 100644 ui/src/assets/icons/file-types/qwik.svg
create mode 100644 ui/src/assets/icons/file-types/r.svg
create mode 100644 ui/src/assets/icons/file-types/racket.svg
create mode 100644 ui/src/assets/icons/file-types/raml.svg
create mode 100644 ui/src/assets/icons/file-types/razor.svg
create mode 100644 ui/src/assets/icons/file-types/rbxmk.svg
create mode 100644 ui/src/assets/icons/file-types/rc.svg
create mode 100644 ui/src/assets/icons/file-types/react.svg
create mode 100644 ui/src/assets/icons/file-types/react_ts.svg
create mode 100644 ui/src/assets/icons/file-types/readme.svg
create mode 100644 ui/src/assets/icons/file-types/reason.svg
create mode 100644 ui/src/assets/icons/file-types/red.svg
create mode 100644 ui/src/assets/icons/file-types/redux-action.svg
create mode 100644 ui/src/assets/icons/file-types/redux-reducer.svg
create mode 100644 ui/src/assets/icons/file-types/redux-selector.svg
create mode 100644 ui/src/assets/icons/file-types/redux-store.svg
create mode 100644 ui/src/assets/icons/file-types/regedit.svg
create mode 100644 ui/src/assets/icons/file-types/remark.svg
create mode 100644 ui/src/assets/icons/file-types/remix.svg
create mode 100644 ui/src/assets/icons/file-types/remix_light.svg
create mode 100644 ui/src/assets/icons/file-types/renovate.svg
create mode 100644 ui/src/assets/icons/file-types/replit.svg
create mode 100644 ui/src/assets/icons/file-types/rescript-interface.svg
create mode 100644 ui/src/assets/icons/file-types/rescript.svg
create mode 100644 ui/src/assets/icons/file-types/restql.svg
create mode 100644 ui/src/assets/icons/file-types/riot.svg
create mode 100644 ui/src/assets/icons/file-types/roadmap.svg
create mode 100644 ui/src/assets/icons/file-types/roblox.svg
create mode 100644 ui/src/assets/icons/file-types/robot.svg
create mode 100644 ui/src/assets/icons/file-types/robots.svg
create mode 100644 ui/src/assets/icons/file-types/rocket.svg
create mode 100644 ui/src/assets/icons/file-types/rojo.svg
create mode 100644 ui/src/assets/icons/file-types/rollup.svg
create mode 100644 ui/src/assets/icons/file-types/rome.svg
create mode 100644 ui/src/assets/icons/file-types/routing.svg
create mode 100644 ui/src/assets/icons/file-types/rspec.svg
create mode 100644 ui/src/assets/icons/file-types/rubocop.svg
create mode 100644 ui/src/assets/icons/file-types/rubocop_light.svg
create mode 100644 ui/src/assets/icons/file-types/ruby.svg
create mode 100644 ui/src/assets/icons/file-types/ruff.svg
create mode 100644 ui/src/assets/icons/file-types/rust.svg
create mode 100644 ui/src/assets/icons/file-types/salesforce.svg
create mode 100644 ui/src/assets/icons/file-types/san.svg
create mode 100644 ui/src/assets/icons/file-types/sas.svg
create mode 100644 ui/src/assets/icons/file-types/sass.svg
create mode 100644 ui/src/assets/icons/file-types/sbt.svg
create mode 100644 ui/src/assets/icons/file-types/scala.svg
create mode 100644 ui/src/assets/icons/file-types/scheme.svg
create mode 100644 ui/src/assets/icons/file-types/scons.svg
create mode 100644 ui/src/assets/icons/file-types/scons_light.svg
create mode 100644 ui/src/assets/icons/file-types/screwdriver.svg
create mode 100644 ui/src/assets/icons/file-types/search.svg
create mode 100644 ui/src/assets/icons/file-types/semantic-release.svg
create mode 100644 ui/src/assets/icons/file-types/semantic-release_light.svg
create mode 100644 ui/src/assets/icons/file-types/semgrep.svg
create mode 100644 ui/src/assets/icons/file-types/sentry.svg
create mode 100644 ui/src/assets/icons/file-types/sequelize.svg
create mode 100644 ui/src/assets/icons/file-types/serverless.svg
create mode 100644 ui/src/assets/icons/file-types/settings.svg
create mode 100644 ui/src/assets/icons/file-types/shader.svg
create mode 100644 ui/src/assets/icons/file-types/silverstripe.svg
create mode 100644 ui/src/assets/icons/file-types/simulink.svg
create mode 100644 ui/src/assets/icons/file-types/siyuan.svg
create mode 100644 ui/src/assets/icons/file-types/sketch.svg
create mode 100644 ui/src/assets/icons/file-types/slim.svg
create mode 100644 ui/src/assets/icons/file-types/slint.svg
create mode 100644 ui/src/assets/icons/file-types/slug.svg
create mode 100644 ui/src/assets/icons/file-types/smarty.svg
create mode 100644 ui/src/assets/icons/file-types/sml.svg
create mode 100644 ui/src/assets/icons/file-types/snakemake.svg
create mode 100644 ui/src/assets/icons/file-types/snapcraft.svg
create mode 100644 ui/src/assets/icons/file-types/snowpack.svg
create mode 100644 ui/src/assets/icons/file-types/snowpack_light.svg
create mode 100644 ui/src/assets/icons/file-types/snyk.svg
create mode 100644 ui/src/assets/icons/file-types/solidity.svg
create mode 100644 ui/src/assets/icons/file-types/sonarcloud.svg
create mode 100644 ui/src/assets/icons/file-types/sprite.svg
create mode 100644 ui/src/assets/icons/file-types/spwn.svg
create mode 100644 ui/src/assets/icons/file-types/stackblitz.svg
create mode 100644 ui/src/assets/icons/file-types/stan.svg
create mode 100644 ui/src/assets/icons/file-types/steadybit.svg
create mode 100644 ui/src/assets/icons/file-types/stencil.svg
create mode 100644 ui/src/assets/icons/file-types/stitches.svg
create mode 100644 ui/src/assets/icons/file-types/stitches_light.svg
create mode 100644 ui/src/assets/icons/file-types/storybook.svg
create mode 100644 ui/src/assets/icons/file-types/stryker.svg
create mode 100644 ui/src/assets/icons/file-types/stylable.svg
create mode 100644 ui/src/assets/icons/file-types/stylelint.svg
create mode 100644 ui/src/assets/icons/file-types/stylelint_light.svg
create mode 100644 ui/src/assets/icons/file-types/stylus.svg
create mode 100644 ui/src/assets/icons/file-types/sublime.svg
create mode 100644 ui/src/assets/icons/file-types/subtitles.svg
create mode 100644 ui/src/assets/icons/file-types/supabase.svg
create mode 100644 ui/src/assets/icons/file-types/svelte.svg
create mode 100644 ui/src/assets/icons/file-types/svg.svg
create mode 100644 ui/src/assets/icons/file-types/svgo.svg
create mode 100644 ui/src/assets/icons/file-types/svgr.svg
create mode 100644 ui/src/assets/icons/file-types/swagger.svg
create mode 100644 ui/src/assets/icons/file-types/sway.svg
create mode 100644 ui/src/assets/icons/file-types/swc.svg
create mode 100644 ui/src/assets/icons/file-types/swift.svg
create mode 100644 ui/src/assets/icons/file-types/syncpack.svg
create mode 100644 ui/src/assets/icons/file-types/systemd.svg
create mode 100644 ui/src/assets/icons/file-types/systemd_light.svg
create mode 100644 ui/src/assets/icons/file-types/table.svg
create mode 100644 ui/src/assets/icons/file-types/tailwindcss.svg
create mode 100644 ui/src/assets/icons/file-types/taskfile.svg
create mode 100644 ui/src/assets/icons/file-types/tauri.svg
create mode 100644 ui/src/assets/icons/file-types/taze.svg
create mode 100644 ui/src/assets/icons/file-types/tcl.svg
create mode 100644 ui/src/assets/icons/file-types/teal.svg
create mode 100644 ui/src/assets/icons/file-types/templ.svg
create mode 100644 ui/src/assets/icons/file-types/template.svg
create mode 100644 ui/src/assets/icons/file-types/terraform.svg
create mode 100644 ui/src/assets/icons/file-types/test-js.svg
create mode 100644 ui/src/assets/icons/file-types/test-jsx.svg
create mode 100644 ui/src/assets/icons/file-types/test-ts.svg
create mode 100644 ui/src/assets/icons/file-types/tex.svg
create mode 100644 ui/src/assets/icons/file-types/textlint.svg
create mode 100644 ui/src/assets/icons/file-types/tilt.svg
create mode 100644 ui/src/assets/icons/file-types/tldraw.svg
create mode 100644 ui/src/assets/icons/file-types/tldraw_light.svg
create mode 100644 ui/src/assets/icons/file-types/tobi.svg
create mode 100644 ui/src/assets/icons/file-types/tobimake.svg
create mode 100644 ui/src/assets/icons/file-types/todo.svg
create mode 100644 ui/src/assets/icons/file-types/toml.svg
create mode 100644 ui/src/assets/icons/file-types/toml_light.svg
create mode 100644 ui/src/assets/icons/file-types/travis.svg
create mode 100644 ui/src/assets/icons/file-types/tree.svg
create mode 100644 ui/src/assets/icons/file-types/trigger.svg
create mode 100644 ui/src/assets/icons/file-types/tsconfig.svg
create mode 100644 ui/src/assets/icons/file-types/tsdoc.svg
create mode 100644 ui/src/assets/icons/file-types/tsil.svg
create mode 100644 ui/src/assets/icons/file-types/tune.svg
create mode 100644 ui/src/assets/icons/file-types/turborepo.svg
create mode 100644 ui/src/assets/icons/file-types/turborepo_light.svg
create mode 100644 ui/src/assets/icons/file-types/twig.svg
create mode 100644 ui/src/assets/icons/file-types/twine.svg
create mode 100644 ui/src/assets/icons/file-types/typescript-def.svg
create mode 100644 ui/src/assets/icons/file-types/typescript.svg
create mode 100644 ui/src/assets/icons/file-types/typst.svg
create mode 100644 ui/src/assets/icons/file-types/umi.svg
create mode 100644 ui/src/assets/icons/file-types/uml.svg
create mode 100644 ui/src/assets/icons/file-types/uml_light.svg
create mode 100644 ui/src/assets/icons/file-types/unity.svg
create mode 100644 ui/src/assets/icons/file-types/unocss.svg
create mode 100644 ui/src/assets/icons/file-types/url.svg
create mode 100644 ui/src/assets/icons/file-types/uv.svg
create mode 100644 ui/src/assets/icons/file-types/vagrant.svg
create mode 100644 ui/src/assets/icons/file-types/vala.svg
create mode 100644 ui/src/assets/icons/file-types/vanilla-extract.svg
create mode 100644 ui/src/assets/icons/file-types/varnish.svg
create mode 100644 ui/src/assets/icons/file-types/vedic.svg
create mode 100644 ui/src/assets/icons/file-types/velite.svg
create mode 100644 ui/src/assets/icons/file-types/velocity.svg
create mode 100644 ui/src/assets/icons/file-types/vercel.svg
create mode 100644 ui/src/assets/icons/file-types/vercel_light.svg
create mode 100644 ui/src/assets/icons/file-types/verdaccio.svg
create mode 100644 ui/src/assets/icons/file-types/verified.svg
create mode 100644 ui/src/assets/icons/file-types/verilog.svg
create mode 100644 ui/src/assets/icons/file-types/vfl.svg
create mode 100644 ui/src/assets/icons/file-types/video.svg
create mode 100644 ui/src/assets/icons/file-types/vim.svg
create mode 100644 ui/src/assets/icons/file-types/virtual.svg
create mode 100644 ui/src/assets/icons/file-types/visualstudio.svg
create mode 100644 ui/src/assets/icons/file-types/vite.svg
create mode 100644 ui/src/assets/icons/file-types/vitest.svg
create mode 100644 ui/src/assets/icons/file-types/vlang.svg
create mode 100644 ui/src/assets/icons/file-types/vscode.svg
create mode 100644 ui/src/assets/icons/file-types/vue-config.svg
create mode 100644 ui/src/assets/icons/file-types/vue.svg
create mode 100644 ui/src/assets/icons/file-types/vuex-store.svg
create mode 100644 ui/src/assets/icons/file-types/wakatime.svg
create mode 100644 ui/src/assets/icons/file-types/wakatime_light.svg
create mode 100644 ui/src/assets/icons/file-types/wallaby.svg
create mode 100644 ui/src/assets/icons/file-types/wally.svg
create mode 100644 ui/src/assets/icons/file-types/watchman.svg
create mode 100644 ui/src/assets/icons/file-types/webassembly.svg
create mode 100644 ui/src/assets/icons/file-types/webhint.svg
create mode 100644 ui/src/assets/icons/file-types/webpack.svg
create mode 100644 ui/src/assets/icons/file-types/wepy.svg
create mode 100644 ui/src/assets/icons/file-types/werf.svg
create mode 100644 ui/src/assets/icons/file-types/windicss.svg
create mode 100644 ui/src/assets/icons/file-types/wolframlanguage.svg
create mode 100644 ui/src/assets/icons/file-types/word.svg
create mode 100644 ui/src/assets/icons/file-types/wrangler.svg
create mode 100644 ui/src/assets/icons/file-types/wxt.svg
create mode 100644 ui/src/assets/icons/file-types/xaml.svg
create mode 100644 ui/src/assets/icons/file-types/xmake.svg
create mode 100644 ui/src/assets/icons/file-types/xml.svg
create mode 100644 ui/src/assets/icons/file-types/yaml.svg
create mode 100644 ui/src/assets/icons/file-types/yang.svg
create mode 100644 ui/src/assets/icons/file-types/yarn.svg
create mode 100644 ui/src/assets/icons/file-types/zeabur.svg
create mode 100644 ui/src/assets/icons/file-types/zeabur_light.svg
create mode 100644 ui/src/assets/icons/file-types/zig.svg
create mode 100644 ui/src/assets/icons/file-types/zip.svg
create mode 100644 ui/src/assets/provider-logos/bailian-coding-plan.svg
create mode 100644 ui/src/assets/provider-logos/evroc.svg
create mode 100644 ui/src/assets/provider-logos/gocode.svg
create mode 100644 ui/src/components/auth/SessionAuthGate.tsx
create mode 100644 ui/src/components/chat/AgentMentionAutocomplete.tsx
create mode 100644 ui/src/components/chat/ChatContainer.tsx
create mode 100644 ui/src/components/chat/ChatEmptyState.tsx
create mode 100644 ui/src/components/chat/ChatErrorBoundary.tsx
create mode 100644 ui/src/components/chat/ChatInput.tsx
create mode 100644 ui/src/components/chat/ChatMessage.tsx
create mode 100644 ui/src/components/chat/CommandAutocomplete.tsx
create mode 100644 ui/src/components/chat/DiffPreview.tsx
create mode 100644 ui/src/components/chat/FileAttachment.tsx
create mode 100644 ui/src/components/chat/FileMentionAutocomplete.tsx
create mode 100644 ui/src/components/chat/MarkdownRenderer.tsx
create mode 100644 ui/src/components/chat/MessageList.tsx
create mode 100644 ui/src/components/chat/MobileAgentButton.tsx
create mode 100644 ui/src/components/chat/MobileModelButton.tsx
create mode 100644 ui/src/components/chat/MobileSessionStatusBar.tsx
create mode 100644 ui/src/components/chat/ModelControls.tsx
create mode 100644 ui/src/components/chat/PermissionCard.tsx
create mode 100644 ui/src/components/chat/PermissionRequest.tsx
create mode 100644 ui/src/components/chat/PermissionToastActions.tsx
create mode 100644 ui/src/components/chat/QuestionCard.tsx
create mode 100644 ui/src/components/chat/QueuedMessageChips.tsx
create mode 100644 ui/src/components/chat/SkillAutocomplete.tsx
create mode 100644 ui/src/components/chat/StatusChip.tsx
create mode 100644 ui/src/components/chat/StatusRow.tsx
create mode 100644 ui/src/components/chat/StreamingTextDiff.tsx
create mode 100644 ui/src/components/chat/TimelineDialog.tsx
create mode 100644 ui/src/components/chat/UnifiedControlsDrawer.tsx
create mode 100644 ui/src/components/chat/contexts/TurnGroupingContext.tsx
create mode 100644 ui/src/components/chat/hooks/useTurnGrouping.ts
create mode 100644 ui/src/components/chat/message/DiffViewToggle.tsx
create mode 100644 ui/src/components/chat/message/FadeInOnReveal.tsx
create mode 100644 ui/src/components/chat/message/MessageBody.tsx
create mode 100644 ui/src/components/chat/message/MessageHeader.tsx
create mode 100644 ui/src/components/chat/message/TextSelectionMenu.tsx
create mode 100644 ui/src/components/chat/message/ToolOutputDialog.tsx
create mode 100644 ui/src/components/chat/message/messageRole.ts
create mode 100644 ui/src/components/chat/message/partUtils.ts
create mode 100644 ui/src/components/chat/message/parts/AssistantTextPart.tsx
create mode 100644 ui/src/components/chat/message/parts/JustificationBlock.tsx
create mode 100644 ui/src/components/chat/message/parts/MigratingPart.tsx
create mode 100644 ui/src/components/chat/message/parts/ProgressiveGroup.tsx
create mode 100644 ui/src/components/chat/message/parts/ReasoningPart.tsx
create mode 100644 ui/src/components/chat/message/parts/ToolPart.tsx
create mode 100644 ui/src/components/chat/message/parts/UserTextPart.tsx
create mode 100644 ui/src/components/chat/message/parts/VirtualizedCodeBlock.tsx
create mode 100644 ui/src/components/chat/message/parts/WorkingPlaceholder.tsx
create mode 100644 ui/src/components/chat/message/timeFormat.ts
create mode 100644 ui/src/components/chat/message/toolRenderers.tsx
create mode 100644 ui/src/components/chat/message/types.ts
create mode 100644 ui/src/components/chat/mobileControlsUtils.ts
create mode 100644 ui/src/components/comments/CodeMirrorCommentWidgets.tsx
create mode 100644 ui/src/components/comments/InlineCommentCard.tsx
create mode 100644 ui/src/components/comments/InlineCommentInput.tsx
create mode 100644 ui/src/components/comments/PierreDiffCommentOverlays.tsx
create mode 100644 ui/src/components/comments/PierreDiffCommentUtils.ts
create mode 100644 ui/src/components/comments/index.ts
create mode 100644 ui/src/components/comments/useInlineCommentController.ts
create mode 100644 ui/src/components/desktop/DesktopHostSwitcher.tsx
create mode 100644 ui/src/components/desktop/OpenInAppButton.tsx
create mode 100644 ui/src/components/icons/ArrowsMerge.tsx
create mode 100644 ui/src/components/icons/DiffIcon.tsx
create mode 100644 ui/src/components/icons/FileTypeIcon.tsx
create mode 100644 ui/src/components/icons/McpIcon.tsx
create mode 100644 ui/src/components/icons/StopIcon.tsx
create mode 100644 ui/src/components/layout/BottomTerminalDock.tsx
create mode 100644 ui/src/components/layout/ContextPanel.tsx
create mode 100644 ui/src/components/layout/ContextSidebarTab.tsx
create mode 100644 ui/src/components/layout/Header.tsx
create mode 100644 ui/src/components/layout/MainLayout.tsx
create mode 100644 ui/src/components/layout/NavRail.tsx
create mode 100644 ui/src/components/layout/ProjectActionsButton.tsx
create mode 100644 ui/src/components/layout/ProjectEditDialog.tsx
create mode 100644 ui/src/components/layout/RightSidebar.tsx
create mode 100644 ui/src/components/layout/RightSidebarTabs.tsx
create mode 100644 ui/src/components/layout/Sidebar.tsx
create mode 100644 ui/src/components/layout/SidebarContextSummary.tsx
create mode 100644 ui/src/components/layout/SidebarFilesTree.tsx
create mode 100644 ui/src/components/layout/VSCodeLayout.tsx
create mode 100644 ui/src/components/mcp/McpDropdown.tsx
create mode 100644 ui/src/components/multirun/AgentSelector.tsx
create mode 100644 ui/src/components/multirun/BranchSelector.tsx
create mode 100644 ui/src/components/multirun/ModelMultiSelect.tsx
create mode 100644 ui/src/components/multirun/MultiRunLauncher.tsx
create mode 100644 ui/src/components/multirun/index.ts
create mode 100644 ui/src/components/onboarding/OnboardingScreen.tsx
create mode 100644 ui/src/components/providers/ThemeProvider.tsx
create mode 100644 ui/src/components/sections/SectionPlaceholder.tsx
create mode 100644 ui/src/components/sections/agents/AgentsPage.tsx
create mode 100644 ui/src/components/sections/agents/AgentsSidebar.tsx
create mode 100644 ui/src/components/sections/agents/ModelSelector.tsx
create mode 100644 ui/src/components/sections/commands/AgentSelector.tsx
create mode 100644 ui/src/components/sections/commands/CommandsPage.tsx
create mode 100644 ui/src/components/sections/commands/CommandsSidebar.tsx
create mode 100644 ui/src/components/sections/git-identities/GitIdentityEditorDialog.tsx
create mode 100644 ui/src/components/sections/git-identities/GitPage.tsx
create mode 100644 ui/src/components/sections/mcp/McpPage.tsx
create mode 100644 ui/src/components/sections/mcp/McpSidebar.tsx
create mode 100644 ui/src/components/sections/openchamber/AboutSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/DefaultsSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/GitHubSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/GitSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/KeyboardShortcutsSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/MemoryLimitsSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/NotificationSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/OpenChamberPage.tsx
create mode 100644 ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/OpenCodeCliSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/SessionRetentionSettings.tsx
create mode 100644 ui/src/components/sections/openchamber/WorktreeSectionContent.tsx
create mode 100644 ui/src/components/sections/openchamber/types.ts
create mode 100644 ui/src/components/sections/projects/ProjectActionsSection.tsx
create mode 100644 ui/src/components/sections/projects/ProjectsPage.tsx
create mode 100644 ui/src/components/sections/projects/ProjectsSidebar.tsx
create mode 100644 ui/src/components/sections/providers/ProvidersPage.tsx
create mode 100644 ui/src/components/sections/providers/ProvidersSidebar.tsx
create mode 100644 ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx
create mode 100644 ui/src/components/sections/remote-instances/RemoteInstancesSidebar.tsx
create mode 100644 ui/src/components/sections/shared/SettingsPageLayout.tsx
create mode 100644 ui/src/components/sections/shared/SettingsProjectSelector.tsx
create mode 100644 ui/src/components/sections/shared/SettingsSection.tsx
create mode 100644 ui/src/components/sections/shared/SettingsSidebarHeader.tsx
create mode 100644 ui/src/components/sections/shared/SettingsSidebarItem.tsx
create mode 100644 ui/src/components/sections/shared/SettingsSidebarLayout.tsx
create mode 100644 ui/src/components/sections/shared/SidebarGroup.tsx
create mode 100644 ui/src/components/sections/shared/index.ts
create mode 100644 ui/src/components/sections/skills/SkillsPage.tsx
create mode 100644 ui/src/components/sections/skills/SkillsSidebar.tsx
create mode 100644 ui/src/components/sections/skills/catalog/AddCatalogDialog.tsx
create mode 100644 ui/src/components/sections/skills/catalog/InstallConflictsDialog.tsx
create mode 100644 ui/src/components/sections/skills/catalog/InstallFromRepoDialog.tsx
create mode 100644 ui/src/components/sections/skills/catalog/InstallSkillDialog.tsx
create mode 100644 ui/src/components/sections/skills/catalog/SkillsCatalogPage.tsx
create mode 100644 ui/src/components/sections/skills/index.ts
create mode 100644 ui/src/components/sections/skills/skillLocations.ts
create mode 100644 ui/src/components/sections/usage/PaceIndicator.tsx
create mode 100644 ui/src/components/sections/usage/UsageCard.tsx
create mode 100644 ui/src/components/sections/usage/UsagePage.tsx
create mode 100644 ui/src/components/sections/usage/UsageProgressBar.tsx
create mode 100644 ui/src/components/sections/usage/UsageSidebar.tsx
create mode 100644 ui/src/components/session/BranchPickerDialog.tsx
create mode 100644 ui/src/components/session/DirectoryAutocomplete.tsx
create mode 100644 ui/src/components/session/DirectoryExplorerDialog.tsx
create mode 100644 ui/src/components/session/DirectoryTree.tsx
create mode 100644 ui/src/components/session/GitHubIntegrationDialog.tsx
create mode 100644 ui/src/components/session/GitHubIssuePickerDialog.tsx
create mode 100644 ui/src/components/session/GitHubPrPickerDialog.tsx
create mode 100644 ui/src/components/session/NewWorktreeDialog.tsx
create mode 100644 ui/src/components/session/ProjectNotesTodoPanel.tsx
create mode 100644 ui/src/components/session/SessionDialogs.tsx
create mode 100644 ui/src/components/session/SessionFolderItem.tsx
create mode 100644 ui/src/components/session/SessionSidebar.tsx
create mode 100644 ui/src/components/session/sidebar/ConfirmDialogs.tsx
create mode 100644 ui/src/components/session/sidebar/DOCUMENTATION.md
create mode 100644 ui/src/components/session/sidebar/SessionGroupSection.tsx
create mode 100644 ui/src/components/session/sidebar/SessionNodeItem.tsx
create mode 100644 ui/src/components/session/sidebar/SidebarHeader.tsx
create mode 100644 ui/src/components/session/sidebar/SidebarProjectsList.tsx
create mode 100644 ui/src/components/session/sidebar/hooks/useArchivedAutoFolders.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useDirectoryStatusProbe.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useGroupOrdering.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useProjectRepoStatus.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useProjectSessionLists.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useProjectSessionSelection.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useSessionActions.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useSessionFolderCleanup.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useSessionGrouping.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useSessionPrefetch.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useSessionSearchEffects.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useSessionSidebarSections.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useSidebarPersistence.ts
create mode 100644 ui/src/components/session/sidebar/hooks/useStickyProjectHeaders.ts
create mode 100644 ui/src/components/session/sidebar/sessionFolderDnd.tsx
create mode 100644 ui/src/components/session/sidebar/sortableItems.tsx
create mode 100644 ui/src/components/session/sidebar/types.ts
create mode 100644 ui/src/components/session/sidebar/utils.tsx
create mode 100644 ui/src/components/terminal/TerminalViewport.tsx
create mode 100644 ui/src/components/ui/AboutDialog.tsx
create mode 100644 ui/src/components/ui/CodeMirrorEditor.tsx
create mode 100644 ui/src/components/ui/CommandPalette.tsx
create mode 100644 ui/src/components/ui/ConfigUpdateOverlay.tsx
create mode 100644 ui/src/components/ui/ContextUsageDisplay.tsx
create mode 100644 ui/src/components/ui/ErrorBoundary.tsx
create mode 100644 ui/src/components/ui/FireworksAnimation.tsx
create mode 100644 ui/src/components/ui/HelpDialog.tsx
create mode 100644 ui/src/components/ui/MemoryDebugPanel.tsx
create mode 100644 ui/src/components/ui/MobileOverlayPanel.tsx
create mode 100644 ui/src/components/ui/OpenChamberLogo.tsx
create mode 100644 ui/src/components/ui/OpenCodeIcon.tsx
create mode 100644 ui/src/components/ui/OpenCodeLogo.tsx
create mode 100644 ui/src/components/ui/OpenCodeStatusDialog.tsx
create mode 100644 ui/src/components/ui/OverlayScrollbar.tsx
create mode 100644 ui/src/components/ui/ProviderLogo.tsx
create mode 100644 ui/src/components/ui/ScrollShadow.tsx
create mode 100644 ui/src/components/ui/ScrollableOverlay.tsx
create mode 100644 ui/src/components/ui/TextLoop.tsx
create mode 100644 ui/src/components/ui/UpdateDialog.tsx
create mode 100644 ui/src/components/ui/alert.tsx
create mode 100644 ui/src/components/ui/button-large.tsx
create mode 100644 ui/src/components/ui/button-small.tsx
create mode 100644 ui/src/components/ui/button.tsx
create mode 100644 ui/src/components/ui/card.tsx
create mode 100644 ui/src/components/ui/checkbox.tsx
create mode 100644 ui/src/components/ui/collapsible.tsx
create mode 100644 ui/src/components/ui/command.tsx
create mode 100644 ui/src/components/ui/dialog.tsx
create mode 100644 ui/src/components/ui/dropdown-menu.tsx
create mode 100644 ui/src/components/ui/grid-loader.tsx
create mode 100644 ui/src/components/ui/index.ts
create mode 100644 ui/src/components/ui/input.tsx
create mode 100644 ui/src/components/ui/number-input.tsx
create mode 100644 ui/src/components/ui/radio.tsx
create mode 100644 ui/src/components/ui/scroll-area.tsx
create mode 100644 ui/src/components/ui/select.tsx
create mode 100644 ui/src/components/ui/separator.tsx
create mode 100644 ui/src/components/ui/skeleton.tsx
create mode 100644 ui/src/components/ui/slider.tsx
create mode 100644 ui/src/components/ui/sonner.tsx
create mode 100644 ui/src/components/ui/sortable-tabs-strip.tsx
create mode 100644 ui/src/components/ui/switch.tsx
create mode 100644 ui/src/components/ui/text.tsx
create mode 100644 ui/src/components/ui/textarea.tsx
create mode 100644 ui/src/components/ui/toast.ts
create mode 100644 ui/src/components/ui/toggle.tsx
create mode 100644 ui/src/components/ui/tooltip.tsx
create mode 100644 ui/src/components/ui/typewriter-text.tsx
create mode 100644 ui/src/components/views/ChatView.tsx
create mode 100644 ui/src/components/views/DiffView.tsx
create mode 100644 ui/src/components/views/FilesView.tsx
create mode 100644 ui/src/components/views/GitView.tsx
create mode 100644 ui/src/components/views/PierreDiffViewer.tsx
create mode 100644 ui/src/components/views/PlanView.tsx
create mode 100644 ui/src/components/views/PreviewToggleButton.tsx
create mode 100644 ui/src/components/views/SettingsView.tsx
create mode 100644 ui/src/components/views/SettingsWindow.tsx
create mode 100644 ui/src/components/views/TerminalView.tsx
create mode 100644 ui/src/components/views/agent-manager/AgentGroupDetail.tsx
create mode 100644 ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx
create mode 100644 ui/src/components/views/agent-manager/AgentManagerSidebar.tsx
create mode 100644 ui/src/components/views/agent-manager/AgentManagerView.tsx
create mode 100644 ui/src/components/views/agent-manager/index.ts
create mode 100644 ui/src/components/views/git/AIHighlightsBox.tsx
create mode 100644 ui/src/components/views/git/BranchIntegrationSection.tsx
create mode 100644 ui/src/components/views/git/BranchSelector.tsx
create mode 100644 ui/src/components/views/git/ChangeRow.tsx
create mode 100644 ui/src/components/views/git/ChangesSection.tsx
create mode 100644 ui/src/components/views/git/CommitInput.tsx
create mode 100644 ui/src/components/views/git/CommitSection.tsx
create mode 100644 ui/src/components/views/git/ConflictDialog.tsx
create mode 100644 ui/src/components/views/git/GitEmptyState.tsx
create mode 100644 ui/src/components/views/git/GitHeader.tsx
create mode 100644 ui/src/components/views/git/HistoryCommitRow.tsx
create mode 100644 ui/src/components/views/git/HistorySection.tsx
create mode 100644 ui/src/components/views/git/InProgressOperationBanner.tsx
create mode 100644 ui/src/components/views/git/IntegrateCommitsSection.tsx
create mode 100644 ui/src/components/views/git/PullRequestSection.tsx
create mode 100644 ui/src/components/views/git/StashDialog.tsx
create mode 100644 ui/src/components/views/git/SyncActions.tsx
create mode 100644 ui/src/components/views/git/WorktreeBranchDisplay.tsx
create mode 100644 ui/src/components/views/git/index.ts
create mode 100644 ui/src/components/views/index.ts
create mode 100644 ui/src/constants/sidebar.ts
create mode 100644 ui/src/contexts/DiffWorkerProvider.tsx
create mode 100644 ui/src/contexts/DrawerContext.tsx
create mode 100644 ui/src/contexts/FireworksContext.tsx
create mode 100644 ui/src/contexts/RuntimeAPIProvider.tsx
create mode 100644 ui/src/contexts/ThemeSystemContext.tsx
create mode 100644 ui/src/contexts/runtimeAPIContext.ts
create mode 100644 ui/src/contexts/runtimeAPIRegistry.ts
create mode 100644 ui/src/contexts/theme-system-context.ts
create mode 100644 ui/src/contexts/useThemeSystem.ts
create mode 100644 ui/src/hooks/useAssistantStatus.ts
create mode 100644 ui/src/hooks/useAssistantTyping.ts
create mode 100644 ui/src/hooks/useAvailableTools.ts
create mode 100644 ui/src/hooks/useChatScrollManager.ts
create mode 100644 ui/src/hooks/useChatSearchDirectory.ts
create mode 100644 ui/src/hooks/useDebouncedValue.ts
create mode 100644 ui/src/hooks/useDrawerSwipe.ts
create mode 100644 ui/src/hooks/useEdgeSwipe.ts
create mode 100644 ui/src/hooks/useEffectiveDirectory.ts
create mode 100644 ui/src/hooks/useEventStream.ts
create mode 100644 ui/src/hooks/useFileSystemAccess.ts
create mode 100644 ui/src/hooks/useFireworks.ts
create mode 100644 ui/src/hooks/useFontPreferences.ts
create mode 100644 ui/src/hooks/useGitHubPrBackgroundTracking.ts
create mode 100644 ui/src/hooks/useGitPolling.tsx
create mode 100644 ui/src/hooks/useGitPollingHook.ts
create mode 100644 ui/src/hooks/useIsTextTruncated.ts
create mode 100644 ui/src/hooks/useKeyboardShortcuts.ts
create mode 100644 ui/src/hooks/useLongPress.ts
create mode 100644 ui/src/hooks/useMenuActions.ts
create mode 100644 ui/src/hooks/useModelLists.ts
create mode 100644 ui/src/hooks/useProviderLogo.ts
create mode 100644 ui/src/hooks/usePushVisibilityBeacon.ts
create mode 100644 ui/src/hooks/useQueuedMessageAutoSend.ts
create mode 100644 ui/src/hooks/useRouter.ts
create mode 100644 ui/src/hooks/useRuntimeAPIs.ts
create mode 100644 ui/src/hooks/useScrollEngine.ts
create mode 100644 ui/src/hooks/useServerSessionStatus.ts
create mode 100644 ui/src/hooks/useSessionActivity.ts
create mode 100644 ui/src/hooks/useSessionAutoCleanup.ts
create mode 100644 ui/src/hooks/useSessionStatusBootstrap.ts
create mode 100644 ui/src/hooks/useWindowTitle.ts
create mode 100644 ui/src/index.css
create mode 100644 ui/src/lib/agentColors.ts
create mode 100644 ui/src/lib/api/types.ts
create mode 100644 ui/src/lib/appearanceAutoSave.ts
create mode 100644 ui/src/lib/appearancePersistence.ts
create mode 100644 ui/src/lib/clipboard.ts
create mode 100644 ui/src/lib/codeTheme.ts
create mode 100644 ui/src/lib/codemirror/flexokiTheme.ts
create mode 100644 ui/src/lib/codemirror/languageByExtension.ts
create mode 100644 ui/src/lib/configSync.ts
create mode 100644 ui/src/lib/configUpdate.ts
create mode 100644 ui/src/lib/contextFileOpenGuard.ts
create mode 100644 ui/src/lib/debug.ts
create mode 100644 ui/src/lib/desktop.ts
create mode 100644 ui/src/lib/desktopHosts.ts
create mode 100644 ui/src/lib/desktopSsh.ts
create mode 100644 ui/src/lib/device.ts
create mode 100644 ui/src/lib/diff/workerFactory.ts
create mode 100644 ui/src/lib/directoryPersistence.ts
create mode 100644 ui/src/lib/directoryShowHidden.ts
create mode 100644 ui/src/lib/execCommands.ts
create mode 100644 ui/src/lib/fileOpenLimits.ts
create mode 100644 ui/src/lib/fileTypeIconIds.ts
create mode 100644 ui/src/lib/fileTypeIcons.ts
create mode 100644 ui/src/lib/filesViewShowGitignored.ts
create mode 100644 ui/src/lib/fontOptions.ts
create mode 100644 ui/src/lib/git/branchNameGenerator.ts
create mode 100644 ui/src/lib/git/integrateWorktreeCommits.ts
create mode 100644 ui/src/lib/gitApi.ts
create mode 100644 ui/src/lib/gitApiHttp.ts
create mode 100644 ui/src/lib/ime.ts
create mode 100644 ui/src/lib/messageCompletion.ts
create mode 100644 ui/src/lib/messageCursorPersistence.ts
create mode 100644 ui/src/lib/messageFreshness.ts
create mode 100644 ui/src/lib/messages/agentMentions.ts
create mode 100644 ui/src/lib/messages/executionMeta.ts
create mode 100644 ui/src/lib/messages/inlineComments.ts
create mode 100644 ui/src/lib/messages/messageText.ts
create mode 100644 ui/src/lib/messages/providerAuthError.ts
create mode 100644 ui/src/lib/messages/synthetic.ts
create mode 100644 ui/src/lib/modelPrefsAutoSave.ts
create mode 100644 ui/src/lib/openCodeStatus.ts
create mode 100644 ui/src/lib/openInApps.ts
create mode 100644 ui/src/lib/openchamberConfig.ts
create mode 100644 ui/src/lib/opencode/client.ts
create mode 100644 ui/src/lib/permissions/editModeColors.ts
create mode 100644 ui/src/lib/permissions/editPermissionDefaults.ts
create mode 100644 ui/src/lib/persistence.ts
create mode 100644 ui/src/lib/projectActions.ts
create mode 100644 ui/src/lib/projectMeta.ts
create mode 100644 ui/src/lib/quota/index.ts
create mode 100644 ui/src/lib/quota/model-families.ts
create mode 100644 ui/src/lib/quota/providers/base.ts
create mode 100644 ui/src/lib/quota/providers/index.ts
create mode 100644 ui/src/lib/quota/utils.ts
create mode 100644 ui/src/lib/router/index.ts
create mode 100644 ui/src/lib/router/parseRoute.ts
create mode 100644 ui/src/lib/router/serializeRoute.ts
create mode 100644 ui/src/lib/router/types.ts
create mode 100644 ui/src/lib/sessionEvents.ts
create mode 100644 ui/src/lib/settings/metadata.ts
create mode 100644 ui/src/lib/shiki/appThemeRegistry.ts
create mode 100644 ui/src/lib/shiki/textMateThemeFromAppTheme.ts
create mode 100644 ui/src/lib/shiki/vscodeTextMateTheme.ts
create mode 100644 ui/src/lib/shortcuts.ts
create mode 100644 ui/src/lib/terminal/SerializeAddon.ts
create mode 100644 ui/src/lib/terminalApi.ts
create mode 100644 ui/src/lib/terminalTheme.ts
create mode 100644 ui/src/lib/theme/cssGenerator.ts
create mode 100644 ui/src/lib/theme/syntaxThemeGenerator.ts
create mode 100644 ui/src/lib/theme/themes/aura-dark.json
create mode 100644 ui/src/lib/theme/themes/aura-light.json
create mode 100644 ui/src/lib/theme/themes/ayu-dark.json
create mode 100644 ui/src/lib/theme/themes/ayu-light.json
create mode 100644 ui/src/lib/theme/themes/carbonfox-dark.json
create mode 100644 ui/src/lib/theme/themes/carbonfox-light.json
create mode 100644 ui/src/lib/theme/themes/catppuccin-dark.json
create mode 100644 ui/src/lib/theme/themes/catppuccin-light.json
create mode 100644 ui/src/lib/theme/themes/dracula-dark.json
create mode 100644 ui/src/lib/theme/themes/dracula-light.json
create mode 100644 ui/src/lib/theme/themes/flexoki-dark.json
create mode 100644 ui/src/lib/theme/themes/flexoki-light.json
create mode 100644 ui/src/lib/theme/themes/gruvbox-dark.json
create mode 100644 ui/src/lib/theme/themes/gruvbox-light.json
create mode 100644 ui/src/lib/theme/themes/index.ts
create mode 100644 ui/src/lib/theme/themes/kanagawa-dark.json
create mode 100644 ui/src/lib/theme/themes/kanagawa-light.json
create mode 100644 ui/src/lib/theme/themes/mono-dark.json
create mode 100644 ui/src/lib/theme/themes/mono-light.json
create mode 100644 ui/src/lib/theme/themes/mono-plus-dark.json
create mode 100644 ui/src/lib/theme/themes/mono-plus-light.json
create mode 100644 ui/src/lib/theme/themes/monokai-dark.json
create mode 100644 ui/src/lib/theme/themes/monokai-light.json
create mode 100644 ui/src/lib/theme/themes/nightowl-dark.json
create mode 100644 ui/src/lib/theme/themes/nightowl-light.json
create mode 100644 ui/src/lib/theme/themes/nord-dark.json
create mode 100644 ui/src/lib/theme/themes/nord-light.json
create mode 100644 ui/src/lib/theme/themes/onedarkpro-dark.json
create mode 100644 ui/src/lib/theme/themes/onedarkpro-light.json
create mode 100644 ui/src/lib/theme/themes/prColors.ts
create mode 100644 ui/src/lib/theme/themes/presets.ts
create mode 100644 ui/src/lib/theme/themes/solarized-dark.json
create mode 100644 ui/src/lib/theme/themes/solarized-light.json
create mode 100644 ui/src/lib/theme/themes/tokyonight-dark.json
create mode 100644 ui/src/lib/theme/themes/tokyonight-light.json
create mode 100644 ui/src/lib/theme/themes/vesper-dark.json
create mode 100644 ui/src/lib/theme/themes/vesper-light.json
create mode 100644 ui/src/lib/theme/themes/vitesse-dark-dark.json
create mode 100644 ui/src/lib/theme/themes/vitesse-light-light.json
create mode 100644 ui/src/lib/theme/vscode/adapter.ts
create mode 100644 ui/src/lib/toolHelpers.ts
create mode 100644 ui/src/lib/typography.ts
create mode 100644 ui/src/lib/typographyWatcher.ts
create mode 100644 ui/src/lib/utils.ts
create mode 100644 ui/src/lib/worktreeSessionCreator.ts
create mode 100644 ui/src/lib/worktrees/worktreeCreate.ts
create mode 100644 ui/src/lib/worktrees/worktreeManager.ts
create mode 100644 ui/src/lib/worktrees/worktreeStatus.ts
create mode 100644 ui/src/main.tsx
create mode 100644 ui/src/stores/contextStore.ts
create mode 100644 ui/src/stores/fileStore.ts
create mode 100644 ui/src/stores/globalSessions.ts
create mode 100644 ui/src/stores/messageQueueStore.ts
create mode 100644 ui/src/stores/messageStore.ts
create mode 100644 ui/src/stores/permissionStore.ts
create mode 100644 ui/src/stores/questionStore.ts
create mode 100644 ui/src/stores/sessionStore.ts
create mode 100644 ui/src/stores/types/sessionTypes.ts
create mode 100644 ui/src/stores/useAgentGroupsStore.ts
create mode 100644 ui/src/stores/useAgentsStore.ts
create mode 100644 ui/src/stores/useCommandsStore.ts
create mode 100644 ui/src/stores/useConfigStore.ts
create mode 100644 ui/src/stores/useDesktopSshStore.ts
create mode 100644 ui/src/stores/useDirectoryStore.ts
create mode 100644 ui/src/stores/useFileSearchStore.ts
create mode 100644 ui/src/stores/useFilesViewTabsStore.ts
create mode 100644 ui/src/stores/useGitHubAuthStore.ts
create mode 100644 ui/src/stores/useGitHubPrStatusStore.ts
create mode 100644 ui/src/stores/useGitIdentitiesStore.ts
create mode 100644 ui/src/stores/useGitStore.ts
create mode 100644 ui/src/stores/useInlineCommentDraftStore.ts
create mode 100644 ui/src/stores/useMcpConfigStore.ts
create mode 100644 ui/src/stores/useMcpStore.ts
create mode 100644 ui/src/stores/useMultiRunStore.ts
create mode 100644 ui/src/stores/useOpenInAppsStore.ts
create mode 100644 ui/src/stores/useProjectsStore.ts
create mode 100644 ui/src/stores/useQuotaStore.ts
create mode 100644 ui/src/stores/useSessionDisplayStore.ts
create mode 100644 ui/src/stores/useSessionFoldersStore.ts
create mode 100644 ui/src/stores/useSessionStore.ts
create mode 100644 ui/src/stores/useSkillsCatalogStore.ts
create mode 100644 ui/src/stores/useSkillsStore.ts
create mode 100644 ui/src/stores/useTerminalStore.ts
create mode 100644 ui/src/stores/useTodoStore.ts
create mode 100644 ui/src/stores/useUIStore.ts
create mode 100644 ui/src/stores/useUpdateStore.ts
create mode 100644 ui/src/stores/utils/contextUtils.ts
create mode 100644 ui/src/stores/utils/messageUtils.ts
create mode 100644 ui/src/stores/utils/permissionUtils.ts
create mode 100644 ui/src/stores/utils/safeStorage.ts
create mode 100644 ui/src/stores/utils/streamDebug.ts
create mode 100644 ui/src/stores/utils/streamingUtils.ts
create mode 100644 ui/src/stores/utils/tokenUtils.ts
create mode 100644 ui/src/styles/design-system.css
create mode 100644 ui/src/styles/fireworks.css
create mode 100644 ui/src/styles/fonts.ts
create mode 100644 ui/src/styles/mobile.css
create mode 100644 ui/src/styles/typography.css
create mode 100644 ui/src/types/codemirror-lang-elixir.d.ts
create mode 100644 ui/src/types/desktop.d.ts
create mode 100644 ui/src/types/electron-context-menu.d.ts
create mode 100644 ui/src/types/ghostty-web.d.ts
create mode 100644 ui/src/types/index.ts
create mode 100644 ui/src/types/multirun.ts
create mode 100644 ui/src/types/permission.ts
create mode 100644 ui/src/types/question.ts
create mode 100644 ui/src/types/quota.ts
create mode 100644 ui/src/types/react-syntax-highlighter-create-element.d.ts
create mode 100644 ui/src/types/streamdown.d.ts
create mode 100644 ui/src/types/theme.ts
create mode 100644 ui/src/types/tool.ts
create mode 100644 ui/src/types/vscode.d.ts
create mode 100644 ui/src/types/worktree.ts
create mode 100644 ui/src/vite-env.d.ts
create mode 100644 ui/tsconfig.json
create mode 100644 vite-theme-plugin.ts
create mode 100644 vite.config.ts
create mode 100644 web/.gitignore
create mode 100644 web/README.md
create mode 100644 web/bin/cli.js
create mode 100644 web/electron-package.json
create mode 100644 web/electron/main.js
create mode 100644 web/electron/package-lock.json
create mode 100644 web/electron/package.json
create mode 100644 web/index.html
create mode 100644 web/package-electron.json
create mode 100644 web/package.json
create mode 100644 web/public/apple-touch-icon-120x120.png
create mode 100644 web/public/apple-touch-icon-152x152.png
create mode 100644 web/public/apple-touch-icon-167x167.png
create mode 100644 web/public/apple-touch-icon-180x180.png
create mode 100644 web/public/apple-touch-icon.png
create mode 100644 web/public/apple-touch-icon.svg
create mode 100644 web/public/favicon-16.png
create mode 100644 web/public/favicon-32.png
create mode 100644 web/public/favicon.png
create mode 100644 web/public/favicon.svg
create mode 100644 web/public/logo-dark-192x192.png
create mode 100644 web/public/logo-dark-512x512.svg
create mode 100644 web/public/logo-light-192x192.png
create mode 100644 web/public/logo-light-512x512.svg
create mode 100644 web/server/TERMINAL_INPUT_WS_PROTOCOL.md
create mode 100644 web/server/index.d.ts
create mode 100644 web/server/index.js
create mode 100644 web/server/lib/git/DOCUMENTATION.md
create mode 100644 web/server/lib/git/credentials.js
create mode 100644 web/server/lib/git/identity-storage.js
create mode 100644 web/server/lib/git/index.js
create mode 100644 web/server/lib/git/service.js
create mode 100644 web/server/lib/github/DOCUMENTATION.md
create mode 100644 web/server/lib/github/auth.js
create mode 100644 web/server/lib/github/device-flow.js
create mode 100644 web/server/lib/github/index.js
create mode 100644 web/server/lib/github/octokit.js
create mode 100644 web/server/lib/github/pr-status.js
create mode 100644 web/server/lib/github/repo/index.js
create mode 100644 web/server/lib/notifications/DOCUMENTATION.md
create mode 100644 web/server/lib/notifications/index.js
create mode 100644 web/server/lib/notifications/message.js
create mode 100644 web/server/lib/notifications/message.test.js
create mode 100644 web/server/lib/opencode/DOCUMENTATION.md
create mode 100644 web/server/lib/opencode/agents.js
create mode 100644 web/server/lib/opencode/auth.js
create mode 100644 web/server/lib/opencode/commands.js
create mode 100644 web/server/lib/opencode/index.js
create mode 100644 web/server/lib/opencode/mcp.js
create mode 100644 web/server/lib/opencode/providers.js
create mode 100644 web/server/lib/opencode/shared.js
create mode 100644 web/server/lib/opencode/skills.js
create mode 100644 web/server/lib/opencode/ui-auth.js
create mode 100644 web/server/lib/package-manager.js
create mode 100644 web/server/lib/quota/DOCUMENTATION.md
create mode 100644 web/server/lib/quota/index.js
create mode 100644 web/server/lib/quota/providers/claude.js
create mode 100644 web/server/lib/quota/providers/codex.js
create mode 100644 web/server/lib/quota/providers/copilot.js
create mode 100644 web/server/lib/quota/providers/google/api.js
create mode 100644 web/server/lib/quota/providers/google/auth.js
create mode 100644 web/server/lib/quota/providers/google/index.js
create mode 100644 web/server/lib/quota/providers/google/transforms.js
create mode 100644 web/server/lib/quota/providers/index.js
create mode 100644 web/server/lib/quota/providers/interface.js
create mode 100644 web/server/lib/quota/providers/kimi.js
create mode 100644 web/server/lib/quota/providers/minimax-cn-coding-plan.js
create mode 100644 web/server/lib/quota/providers/minimax-coding-plan.js
create mode 100644 web/server/lib/quota/providers/nanogpt.js
create mode 100644 web/server/lib/quota/providers/ollama-cloud.js
create mode 100644 web/server/lib/quota/providers/openai.js
create mode 100644 web/server/lib/quota/providers/openrouter.js
create mode 100644 web/server/lib/quota/providers/zai.js
create mode 100644 web/server/lib/quota/utils/auth.js
create mode 100644 web/server/lib/quota/utils/formatters.js
create mode 100644 web/server/lib/quota/utils/index.js
create mode 100644 web/server/lib/quota/utils/transformers.js
create mode 100644 web/server/lib/skills-catalog/DOCUMENTATION.md
create mode 100644 web/server/lib/skills-catalog/cache.js
create mode 100644 web/server/lib/skills-catalog/clawdhub/api.js
create mode 100644 web/server/lib/skills-catalog/clawdhub/index.js
create mode 100644 web/server/lib/skills-catalog/clawdhub/install.js
create mode 100644 web/server/lib/skills-catalog/clawdhub/scan.js
create mode 100644 web/server/lib/skills-catalog/curated-sources.js
create mode 100644 web/server/lib/skills-catalog/git.js
create mode 100644 web/server/lib/skills-catalog/index.js
create mode 100644 web/server/lib/skills-catalog/install.js
create mode 100644 web/server/lib/skills-catalog/scan.js
create mode 100644 web/server/lib/skills-catalog/source.js
create mode 100644 web/server/lib/terminal/DOCUMENTATION.md
create mode 100644 web/server/lib/terminal/index.js
create mode 100644 web/server/lib/terminal/input-ws-protocol.js
create mode 100644 web/server/lib/terminal/input-ws-protocol.test.js
create mode 100644 web/src/api/files.ts
create mode 100644 web/src/api/git.ts
create mode 100644 web/src/api/github.ts
create mode 100644 web/src/api/index.ts
create mode 100644 web/src/api/notifications.ts
create mode 100644 web/src/api/permissions.ts
create mode 100644 web/src/api/push.ts
create mode 100644 web/src/api/settings.ts
create mode 100644 web/src/api/terminal.ts
create mode 100644 web/src/api/tools.ts
create mode 100644 web/src/main.tsx
create mode 100644 web/tsconfig.json
create mode 100644 web/vite.config.ts
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa3cead
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,54 @@
+# Logs
+logs
+*.log
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+node_modules
+dist
+dist-ssr
+release
+*.local
+*.tgz
+*.vsix
+/npm
+/tsc
+/openchamber@*
+local-dev*
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+.opencode/plans/*
+.hive
+
+# Build outputs
+build/
+dist-output/
+.gradle/
+.*.bun-build
+
+# Built webview assets (generated during build)
+src/main/resources/webview/
+
+# IDE
+*.iml
+*.ipr
+*.iws
+
+# Local env
+.env
+.env.local
+
+# IntelliJ plugin (separate project)
+packages/intellij/
+
+# OS
+Thumbs.db
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..a24992d
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+lts
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..9fd5ff7
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,128 @@
+# OpenChamber - AI Agent Reference (verified)
+
+## Core purpose
+OpenChamber is a web-based UI runtime for interacting with an OpenCode server (local auto-start or remote URL). UI uses HTTP + SSE via `@opencode-ai/sdk`.
+
+## Tech stack (source of truth: `package.json`, resolved: `bun.lock`)
+- Runtime/tooling: Bun (`package.json` `packageManager`), Node >=20 (`package.json` `engines`)
+- UI: React, TypeScript, Vite, Tailwind v4
+- State: Zustand (`ui/src/stores/`)
+- UI primitives: Radix UI (`package.json` deps), HeroUI (`package.json` deps), Remixicon (`package.json` deps)
+- Server: Express (`web/server/index.js`)
+
+## Monorepo layout
+No longer using workspaces (see `package.json`).
+- Shared UI: `ui`
+- Web app + server + CLI: `web`
+
+## Documentation map
+Before changing any mapped module, read its module documentation first.
+
+### web
+Web runtime and server implementation for OpenChamber.
+
+#### lib
+Server-side integration modules used by API routes and runtime services.
+
+##### quota
+Quota provider registry, dispatch, and provider integrations for usage endpoints.
+- Module docs: `web/server/lib/quota/DOCUMENTATION.md`
+
+##### git
+Git repository operations for the web server runtime.
+- Module docs: `web/server/lib/git/DOCUMENTATION.md`
+
+##### github
+GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing.
+- Module docs: `web/server/lib/github/DOCUMENTATION.md`
+
+##### opencode
+OpenCode server integration utilities including config management, provider authentication, and UI authentication.
+- Module docs: `web/server/lib/opencode/DOCUMENTATION.md`
+
+##### notifications
+Notification message preparation utilities for system notifications, including text truncation and optional summarization.
+- Module docs: `web/server/lib/notifications/DOCUMENTATION.md`
+
+##### terminal
+WebSocket protocol utilities for terminal input handling including message normalization, control frame parsing, and rate limiting.
+- Module docs: `web/server/lib/terminal/DOCUMENTATION.md`
+
+##### tts
+Server-side text-to-speech services and summarization helpers for `/api/tts/*` endpoints.
+- Module docs: `web/server/lib/tts/DOCUMENTATION.md`
+
+##### skills-catalog
+Skills catalog management including discovery, installation, and configuration of agent skill packages.
+- Module docs: `web/server/lib/skills-catalog/DOCUMENTATION.md`
+
+## Build / dev commands (verified)
+All scripts are in `package.json`.
+- Validate: `bun run type-check:web`, `bun run lint:web` (or use :ui variants)
+- Build: `bun run build:web` and/or `bun run build:ui`
+- Package for distribution: `bun run package` (outputs to `dist-output/`)
+
+## Distribution
+Run `bun run package` to generate `dist-output/` folder for distribution.
+Users then run:
+```bash
+cd dist-output
+npm install --omit=dev
+npm run start
+```
+
+## Runtime entry points
+- Web bootstrap: `web/src/main.tsx`
+- Web server: `web/server/index.js`
+- Web CLI: `web/bin/cli.js` (package bin: `web/package.json`)
+
+## OpenCode integration
+- UI client wrapper: `ui/src/lib/opencode/client.ts` (imports `@opencode-ai/sdk/v2`)
+- SSE hookup: `ui/src/hooks/useEventStream.ts`
+- Web server embeds/starts OpenCode server: `web/server/index.js` (`createOpencodeServer`)
+- Web runtime filesystem endpoints: search `web/server/index.js` for `/api/fs/`
+- External server support: Set `OPENCODE_HOST` (full base URL, e.g. `http://hostname:4096`) or `OPENCODE_PORT`, plus `OPENCODE_SKIP_START=true`, to connect to existing OpenCode instance
+
+## Key UI patterns (reference files)
+- Settings shell: `ui/src/components/views/SettingsView.tsx`
+- Settings shared primitives: `ui/src/components/sections/shared/`
+- Settings sections: `ui/src/components/sections/` (incl `skills/`)
+- Chat UI: `ui/src/components/chat/` and `ui/src/components/chat/message/`
+- Theme + typography: `ui/src/lib/theme/`, `ui/src/lib/typography.ts`
+- Terminal UI: `ui/src/components/terminal/` (uses `ghostty-web`)
+
+## External / system integrations (active)
+- Git: `ui/src/lib/gitApi.ts`, `web/server/index.js` (`simple-git`)
+- Terminal PTY: `web/server/index.js` (`bun-pty`/`node-pty`)
+- Skills catalog: `web/server/lib/skills-catalog/`, UI: `ui/src/components/sections/skills/`
+
+## Agent constraints
+- Do not modify `../opencode` (separate repo).
+- Do not run git/GitHub commands unless explicitly asked.
+- Keep baseline green (run `bun run type-check:web`, `bun run lint:web`, `bun run build:web` before finalizing changes).
+
+## Development rules
+- Keep diffs tight; avoid drive-by refactors.
+- Follow local precedent; search nearby code first.
+- TypeScript: avoid `any`/blind casts; keep ESLint/TS green.
+- React: prefer function components + hooks; class only when needed (e.g. error boundaries).
+- Control flow: avoid nested ternaries; prefer early returns + `if/else`/`switch`.
+- Styling: Tailwind v4; typography via `ui/src/lib/typography.ts`; theme vars via `ui/src/lib/theme/`.
+- Toasts: use custom toast wrapper from `@/components/ui` (backed by `ui/src/components/ui/toast.ts`); do not import `sonner` directly in feature code.
+- No new deps unless asked.
+- Never add secrets (`.env`, keys) or log sensitive data.
+
+## Theme System (MANDATORY for UI work)
+
+When working on any UI components, styling, or visual changes, agents **MUST** study the theme system skill first.
+
+**Before starting any UI work:**
+```
+skill({ name: "theme-system" })
+```
+
+This skill contains all color tokens, semantic logic, decision tree, and usage patterns. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes.
+
+## Recent changes
+- Releases + high-level changes: `CHANGELOG.md`
+- Recent commits: `git log --oneline` (latest tags: `v1.8.x`)
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..9e647fb
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,776 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+## [1.8.5] - 2026-03-04
+
+- Desktop: startup now opens the app shell much earlier while background services continue loading, so the app feels ready faster after launch.
+- Desktop/macOS: fixed early title updates that could shift traffic-light window controls on startup, keeping native controls stable in their expected position.
+- VSCode: edit-style tool results now open directly in a focused diff view, so you can review generated changes at the first modified line with less manual navigation.
+- VSCode: cleaned up extension settings by removing duplicate display controls and hiding sections that do not apply in the editor environment.
+- Chat: fixed focus-mode composer layout so the footer action row stays pinned and accessible while writing longer prompts.
+- UI/Theming: unified loading logos and startup screens across runtimes, with visuals that better match your active theme.
+- Projects/UI: project icons now follow active theme foreground colors more consistently, improving readability and visual consistency in project lists.
+- Reliability: improved early startup recovery so models and agents are less likely to appear missing right after launch.
+- Tunnel/CLI: fixed one-time Cloudflare tunnel connect links in CLI output for `--try-cf-tunnel`, so remote collaborators can use the printed URL/QR flow successfully (thanks to @plfavreau).
+- Mobile/PWA: respected OS rotation lock by removing forced orientation behavior in the web app shell (thanks to @theluckystrike).
+
+
+## [1.8.4] - 2026-03-04
+
+- Chat: added clickable file-path links in assistant messages (including line targeting), so you can jump from answer text straight to the exact file location (thanks to @yulia-ivashko).
+- Chat: added a new `Changes` tool-output mode that expands edits/patches by default while keeping activity readable, making long runs easier to review (thanks to @iamhenry).
+- Chat: in-progress tools now appear immediately and stay live in collapsed activity view, so active work is visible earlier with stable durations (thanks to @nelsonPires5).
+- Chat: improved long user-message behavior in sticky mode with bounded height, internal scrolling, and cleaner action hit targets for better readability and control.
+- Chat/Files: improved `@` file discovery and mention behavior with project-scoped search and more consistent matching, reducing wrong-project results.
+- Chat/GitHub: added Attach menu actions to link GitHub issues and PRs directly in any session, making it faster to pull ticket/PR context into a prompt.
+- Chat/Files: restored user image previews/fullscreen navigation and improved text-selection action placement on narrow layouts.
+- Shortcuts/Models: added favorite-model cycling shortcuts, so you can switch between starred models without leaving the keyboard (thanks to @iamhenry).
+- Sessions: added active-project session search in the sidebar, with clearer match behavior and easier clearing during filtering (thanks to @KJdotIO).
+- Worktrees/GitHub: streamlined worktree creation with a unified flow for branches, issues, and PR-linked sessions, including cleaner validation and faster branch loading.
+- Worktrees/Git: fixed branch/PR source resolution (including slash-named branches and fork PR heads), so linked worktrees track and push to the correct upstream branch.
+- Git: fixed a PR panel refresh loop that could trigger repeated updates and unstable behavior in the PR section (thanks to @yulia-ivashko).
+- Files/Desktop: improved `Open In` actions from file views/editors, including app selection behavior and tighter integration for opening focused files (thanks to @yulia-ivashko).
+- Mobile/Projects: added long-press project editing with a bottom-sheet panel and drag-to-reorder support for faster project management on mobile (thanks to @Jovines).
+- Web/PWA/Android: added improved install UX with pre-install naming and manifest shortcut updates, so installed web apps feel more customized and project-aware (thanks to @shekohex).
+- UI: interactive controls now consistently show pointer cursors, improving click affordance and reducing ambiguous hover states (thanks to @KJdotIO).
+- Security/Reliability: hardened terminal auth, tightened skill-file path protections, and reduced sensitive request logging exposure for safer day-to-day usage (thanks to @yulia-ivashko).
+
+
+## [1.8.3] - 2026-03-02
+
+- Chat: added user-message display controls for plain-text rendering and sticky headers, so you can tune readability to match your preferences.
+- Chat/UI: overhauled the context panel with reusable tabs and embedded session chat (_beta_), making parallel context work easier without losing place.
+- Chat: improved code block presentation with cleaner action alignment, restored horizontal scrolling, and polished themed highlighting across chat messages and tool output (thanks to @nelsonPires5).
+- Diff: added quick open-in-editor actions from diff views that jump to the first changed line, so it is faster to move from review to edits.
+- Git: refined Git sidebar tab behavior and spacing, plus bulk-revert with confirmations for easier cleanup.
+- Git: fixed commit staging edge cases by filtering stale deleted paths before staging, reducing pathspec commit failures.
+- Git/Worktrees: restored branch rename/edit controls in draft sessions when working in a worktree directory, so branch actions stay available earlier.
+- Chat: model picker now supports collapsible provider groups and remembers expanded state between sessions.
+- Settings: reorganized chat display settings into a more compact two-column layout, so more new options are easier to navigate.
+- Mobile/UI: fixed session-title overflow in compact headers so running/unread indicators and actions remain visible (thanks to @iamhenry).
+
+
+## [1.8.2] - 2026-03-01
+
+- Updates: hardened the self-update flow with safer release handling and fallback behavior, reducing failed or stuck updates.
+- Chat: added a new "Share as image" action so you can quickly export and share important messages (thanks to @Jovines).
+- Chat: improved message readability with cleaner tool/reasoning rendering and less noisy activity timing in busy conversations (thanks to @nelsonPires5).
+- Desktop/Chat: permission toasts now include session context and a clearer permission preview, making approvals more accessible outside of a session (thanks to @nelsonPires5).
+- VSCode: fixed live streaming edge cases for event endpoints with query/trailing-slash variants, improving real-time updates in chat, session editor, and agent-manager views.
+- Reliability: improved event-stream/session visibility handling when the app is hidden or restored, reducing stale activity states and missed updates.
+- Windows: fixed CLI/runtime path and spawn edge cases to reduce startup and command failures on Windows (thanks to @plfavreau).
+- Notifications/Voice: consolidated TTS and summarization service wiring for steadier text-to-speech and summary flows (thanks to @nelsonPires5).
+- Deployment: fixed Docker build/runtime issues for more reliable containerized setups (thanks to @nzlov).
+
+
+## [1.8.1] - 2026-02-28
+
+- Web/Auth: fixed an issue where non-tunnel browser sessions could incorrectly show a tunnel-only lock screen; normal auth flow now appears unless a tunnel is actually active.
+
+
+## [1.8.0] - 2026-02-28
+
+- Desktop: added SSH remote instance support with dedicated lifecycle and UX flows, so you can work against remote machines more reliably (thanks to @shekohex).
+- Projects: added project icon customization with upload/remove and automatic favicon discovery from your repository (thanks to @shekohex).
+- Projects: added header project actions on Web and Mobile, so you can run and stop any configured project commands without leaving chat.
+- Projects/Desktop: project actions can also open SSH-forwarded URLs, making remote dev-server workflows quicker from inside the app.
+- Desktop: added dynamic window titles that reflect active project and remote context, so it is easier to track where you are working (thanks to @shekohex).
+- Remote Tunnel: added tunnel settings with quick/named modes, secure one-time connect links (with QR), and saved named-tunnel presets/tokens so enabling remote access is easier and safer (thanks to @yulia-ivashko).
+- UI: expanded sprite-based file and folder icons across Files, Diff, and Git views for faster visual scanning (thanks to @shekohex).
+- UI: added an expandable project rail with project names, a settings toggle, and saved expansion state for easier navigation in multi-project setups (thanks to @nguyenngothuong).
+- UI/Files: added file-type icons across file lists, tabs, and diffs, so you can identify files faster at a glance (thanks to @shekohex).
+- Files: added a read-only highlighted view with a quick toggle back to edit mode, so you can quickly review code with richer syntax rendering if you don't need to edit thing (thanks to @shekohex).
+- Files: markdown preview now handles frontmatter more cleanly, improving readability for docs-heavy repos (thanks to @shekohex).
+- Chat: improved long-session performance with virtualized message rendering, smoother scrolling, and more stable behavior in large histories (thanks to @shekohex).
+- Chat: enabled markdown rendering in user messages for clearer formatted prompts and notes (thanks to @haofeng0705).
+- Chat: enabled bueatiful diffs for edit tools in chat making this aligned with dedicated diffs view style (thanks to @shekohex).
+- Chat: pasted absolute paths are now treated as normal messages, reducing accidental command-like behavior when sharing paths.
+- Chat: fixed queued sends for inactive sessions, reducing stuck queues.
+- Chat: upgraded Mermaid rendering with a cleaner diagram view plus quick copy/download actions, making generated diagrams easier to read and share (thanks to @shekohex).
+- Notifications: improved child-session notification detection to reduce missed or misclassified subtask updates (thanks to @Jovines).
+- Deployment: added Docker deployment support with safer container defaults and terminal shell fallback, making self-hosted setups easier to run (thanks to @nzlov).
+- Reliability: improved Windows compatibility across git status checks, OpenCode startup, path normalization, and session merge behavior (thanks to @mmereu).
+- Usage: added MiniMax coding-plan quota provider support for broader usage tracking coverage (thanks to @nzlov).
+- Usage: added Ollama Cloud quota provider support for broader usage tracking coverage (thanks to @iamhenry).
+
+
+## [1.7.5] - 2026-02-25
+
+- UI: moved projects into a dedicated sidebar rail and tightened the layout so switching projects and sessions feels faster.
+- Chat: fixed an issue where messages could occasionally duplicate or disappear during active conversations.
+- Sessions: reduced session-switching overhead to make chat context changes feel more immediate.
+- Reliability/Auth: migrated session auth storage to signed JWTs with a persistent secret, reducing unexpected auth-state drift after reconnects or reloads (thanks to @Jovines).
+- Mobile: pending permission prompts now recover after reconnect/resume instead of getting lost mid-run (thanks to @nelsonPires5).
+- Mobile/Chat: refined message spacing and removed the top scroll shadow for a cleaner small-screen reading experience (thanks to @Jovines).
+- Web: added `OPENCODE_HOST` support so you can connect directly to an external OpenCode server using a full base URL (thanks to @colinmollenhour).
+- Web/Mobile: fixed in-app update flow in containerized setups so updates apply correctly.
+
+
+## [1.7.4] - 2026-02-24
+
+- Settings: redesigned the settings workspace with flatter, more consistent page layouts so configuration is faster to scan and edit.
+- Settings: improved agents and skills navigation by grouping entries by subfolder for easier management at scale (thanks to @nguyenngothuong).
+- Chat: improved streaming smoothness and stability with buffered updates and runtime fixes, reducing lag, stuck spinners, memory growth, and timeout-related interruptions in long runs (thanks to @nguyenngothuong).
+- Chat: added fullscreen Mermaid preview, persisted default thinking variant selection, and hardened file-preview safety checks for a safer, more predictable message experience (thanks to @yulia-ivashko).
+- Chat: draft text now persists per session, and the input supports an expanded focus mode for longer prompts (thanks to @nguyenngothuong).
+- Sessions: expanded folder management with subfolders, cleaner organization actions, and clearer delete confirmations (thanks to @nguyenngothuong).
+- Settings: added an MCP config manager UI to simplify editing and validating MCP server configuration (thanks to @nguyenngothuong).
+- Git/PR: moved commit-message and PR-description generation to active-session structured output, so generation uses current session context and avoids fragile backend polling.
+- Chat Activity: improved Structured Output tool rendering with dedicated title/icon, clearer result descriptions, and more reliable detailed expansion defaults.
+- Notifications/Voice: moved utility model controls into AI Summarization as a Zen-only Summarization Model setting.
+- Mobile: refreshed drawer and session-status layouts for better small-screen usability (thanks to @Jovines).
+- Desktop: improved remote instance URL handling for more reliable host/query matching (thanks to @shekohex).
+- Files: added C, C++, and Go language support for syntax-aware rendering in code-heavy workflows (thanks to @fomenks).
+
+
+## [1.7.3] - 2026-02-21
+
+- Settings: added customizable keyboard shortcuts for chat actions, panel toggles, and services, so you can better match OpenChamber to your workflow (thanks to @nelsonPires5).
+- Sessions: added custom folders to group chat sessions, with move/rename/delete flows and persisted collapse state per project (thanks to @nguyenngothuong).
+- Notifications: improved agent progress notifications and permission handling to reduce noisy prompts during active runs (thanks to @nguyenngothuong).
+- Diff/Plans/Files: restored inline comments making more like a GitHub style again (thanks to @nelsonPires5).
+- Terminal: restored terminal text copy behavior, so selecting and copying command output works reliably again (thanks to @shekohex).
+- UI: unified clipboard copy behavior across Desktop app, Web app, and VS Code extension for more consistent copy actions and feedback.
+- Reliability: improved startup environment detection by capturing login-shell environment snapshots, reducing missing PATH/tool issues on launch.
+- Reliability: refactored OpenCode config/auth integration into domain modules for steadier provider auth and command loading flows (thanks to @nelsonPires5).
+
+
+## [1.7.2] - 2026-02-20
+
+- Chat: question prompts now guide you to unanswered items before submit, making tool-question flows faster.
+- Chat: fixed auto-send queue to wait for the active session to be idle before sending, reducing misfires during agent messages.
+- Chat: improved streaming activity rendering and session attention indicators, so active progress and unread signals stay more consistent.
+- UI: added Plan view in the context sidebar panel for quicker access to plan content while you work (thanks to @nelsonPires5).
+- Settings: model variant options now refresh correctly in draft/new-session flows, avoiding stale selections.
+- Reliability: provider auth failures now show clearer re-auth guidance when tokens expire, making recovery faster (thanks to @yulia-ivashko).
+
+
+## [1.7.1] - 2026-02-18
+
+- Chat: slash commands now follow server command semantics (including multiline arguments), so command behavior is more consistent with OpenCode CLI.
+- Chat: added a shell mode triggered by leading `!`, with inline output visibility/copy.
+- Chat: improved delegated-task clarity with richer subtask bubbles, better task-detail rendering, and parent-chat surfacing for child permission/question requests.
+- Chat: improved `@` mention autocomplete by prioritizing agents and cleaning up ordering for faster picks.
+- Skills: discovery now uses OpenCode API as the source of truth with safer fallback scanning, improving installed-state accuracy.
+- Skills: upgraded editing/install UX with better code editing, syntax-aware related files, and clearer location targeting across user/project .opencode and .agents scopes.
+- Mobile: fixed accidental abort right after tapping Send on touch devices, reducing interrupted responses (thanks to @shekohex).
+- Maintenance: removed deprecated GitHub Actions cloud runtime assets and docs to reduce setup confusion (thanks to @yulia-ivashko).
+
+
+## [1.7.0] - 2026-02-17
+
+- Chat: improved live streaming with part-delta updates and smarter auto-follow scrolling, so long responses stay readable while they generate.
+- Chat: Mermaid diagrams now render inline in assistant messages, with quick copy/download actions for easier sharing.
+- UI: added a context overview panel with token usage, cost breakdown, and raw message inspection to make session debugging easier.
+- Sessions: project icon and color customizations now persist reliably across restarts.
+**- Reliability: managed local OpenCode runtimes now use rotated secure auth and tighter lifecycle control across runtimes, reducing stale-process and reconnect issues (thanks to @yulia-ivashko).**
+- Git/GitHub: improved backend reliability for repository and auth operations, helping branch and PR flows stay more predictable (thanks to @nelsonPires5).
+
+
+## [1.6.9] - 2026-02-16
+
+- **UI: redesigned the workspace shell with a context panel, tabbed sidebars, and quicker navigation across chat, files, and reviews, so daily workflows feel more focused.**
+- UI: compact model info in selection (price + capabilities), making model selection faster and more cost-aware (thanks to @nelsonPires5).
+- Chat: fixed files attachment issue and added displaying of excided quota information.
+- Diff: improved large diff rendering and interaction performance for smoother reviews on heavy changesets.
+- Worktrees: shipped an upstream-first flow across supported runtimes, making branch tracking and worktree session setup more predictable (thanks to @yulia-ivashko).
+- Git: improved pull request branch normalization and base/remote resolution to reduce PR setup mismatches (thanks to @gsxdsm).
+- Sessions: added a persistent project notes and todos panel, so key context and follow-ups stay attached to each project (thanks to @gsxdsm).
+- Sessions: introduced the ability to pin sessions within your groups for easy access.
+- Settings: added a configurable Zen model for commit messages generation and summarization of notifications (thanks to @gsxdsm).
+- Usage: added NanoGPT quota support and hardened provider handling for more reliable usage tracking (thanks to @nelsonPires5).
+- Reliability: startup now auto-detects and safely connects to an existing OpenCode server, reducing duplicate-server conflicts (thanks to @ruslan-kurchenko).
+- Desktop: improved day-to-day polish with restored desktop window geometry and posiotion (thanks to @yulia-ivashko).
+- Mobile: fixes for small-screen editor, terminal, and layout overlap issues (thanks to @gsxdsm, @nelsonPires5).
+
+
+## [1.6.8] - 2026-02-12
+
+- Chat: added drag-and-drop attachments with inline image previews, so sharing screenshots and files in prompts feels much faster and more reliable.
+- Sessions: fixed a sidebar issue where draft input could carry over when switching projects, so each workspace keeps cleaner chat context.
+- Chat: improved quick navigation from the sessions list by adding double-click to jump into chat and auto-focus the draft input; also fixed mobile session return behavior (thanks to @gsxdsm).
+- Chat: improved agent/model picking with fuzzy search across names and descriptions, making long lists easier to filter.
+- Usage: corrected Gemini and Antigravity quota source mapping and labels for more accurate usage tracking (thanks to @gsxdsm).
+- Usage: when using remaining-quota mode, usage markers now invert direction to better match how remaining capacity is interpreted (thanks to @gsxdsm).
+- Desktop: fixed project selection in opened remote instances.
+- Desktop: fixed opened remote instances that use HTTP (helpful for instances under tunneling).
+
+
+## [1.6.7] - 2026-02-10
+
+- Voice: added built-in voice input and read-aloud responses with multiple providers, so you can drive chats hands-free when typing is slower (thanks to @gsxdsm).
+- Git: added multi-remote push selection and smarter fork-aware pull request creation to reduce manual branch/remote setup (thanks to @gsxdsm).
+- Usage: added usage pace and prediction indicators in the header and settings, so it is easier to see how quickly quota is moving (thanks to @gsxdsm).
+- Diff/Plans: fixed comment draft collisions and improved multi-line comment editing in plan and file workflows, so feedback is less likely to get lost (thanks to @nelsonPires5).
+- Notifications: stopped firing completion notifications for comment draft edits to reduce noisy alerts during review-heavy sessions (thanks to @nelsonPires5).
+- Settings: added confirmation dialogs for destructive delete/reset actions to prevent accidental data loss.
+- UI: refreshed header and settings layout, improved host switching, and upgraded the editor for smoother day-to-day navigation and editing.
+- Desktop: added multi-window support with a dedicated "New Window" action for parallel work across projects (thanks to @yulia-ivashko).
+- Reliability: fixed message loading edge cases, stabilized voice-mode persistence across restarts, and improved update flow behavior across platforms.
+
+## [1.6.6] - 2026-02-9
+
+- Desktop: redesigned the main workspace with a dedicated Git sidebar and bottom terminal dock, so Git and terminal actions stay in reach while chatting.
+- Desktop: added an `Open In` button to open the current workspace in Finder, Terminal, and supported editors with remembered app preference (thanks to @yulia-ivashko).
+- Header: combined Instance, Usage, and MCP into one services menu for faster access to runtime controls and rate limits while decluttering the header space.
+- Git: added push/pull with remote selection, plus in-app rebase/merge flows with improved remote inference and clearer conflict handling (thanks to @gsxdsm).
+- Git: reorganized the Git workspace with improved in-app PR workflows.
+- Files: improved editing with breadcrumbs, better draft handling, smoother editor interactions, and more reliable directory navigation from file context (thanks to @nelsonPires5).
+- Sessions: improved status behavior, faster mobile session switching with running/unread indicators, and clearer worktree labels when branch name differs (thanks to @Jovines, @gsxdsm).
+- Notifications: added smarter templates with concise summaries, so completion alerts are easier to scan (thanks to @gsxdsm).
+- Usage: added per-model quota breakdowns with collapsible groups, and fixed provider dropdown scrolling (thanks to @nelsonPires5, @gsxdsm).
+- Terminal: improved input responsiveness with a persistent low-latency transport for steadier typing (thanks to @shekohex).
+- Mobile: fixed chat input layout issues on small screens (thanks to @nelsonPires5).
+- Reliability: fixed OpenCode auth pass-through and proxy env handling to reduce intermittent connection/auth issues (thanks to @gsxdsm).
+
+
+## [1.6.5] - 2026-02-6
+
+- Settings: added an OpenCode CLI path override so you can point OpenChamber at a custom/local CLI install.
+- Chat: added arrow-key prompt history and an optional setting to persist input drafts between restarts (thanks to @gsxdsm).
+- Chat: thinking/reasoning blocks now render more consistently, and justification visibility settings now apply reliably (thanks to @gsxdsm).
+- Diff/Plans: added inline comment drafts so you can leave line-level notes and feed them back into requests (thanks to @nelsonPires5).
+- Sessions: you can now rename projects directly from the sidebar, and issue/PR pickers are easier to scan when starting from GitHub context (thanks to @shekohex, @gsxdsm).
+- Worktrees: improved worktree flow reliability, including cleaner handling when a worktree was already removed outside the app (thanks to @gsxdsm).
+- Terminal: improved Android keyboard behavior and removed distracting native caret blink in terminal inputs (thanks to @shekohex).
+- UI: added Vitesse Dark and Vitesse Light theme presets.
+- Reliability: improved OpenCode binary resolution and HOME-path handling across runtimes for steadier local startup.
+
+
+## [1.6.4] - 2026-02-5
+
+- Desktop: switch between local and remote OpenChamber instances, plus a thinner runtime for better feature parity and fewer desktop-only quirks.
+- VSCode: improved Windows PATH resolution and cold-start readiness checks to reduce "stuck loading" for sessions/models/agents.
+- Mobile: split Agent/Model controls and a quick commands button with autocomplete (Commands/Agents/Files) for easier input (thanks to @Jovines, @gsxdsm).
+- Chat: select text in messages to quickly add it to your prompt or start a new session (thanks to @gsxdsm).
+- Diff/Plans: add inline comment drafts so you can annotate specific lines and include those notes in requests (thanks to @nelsonPires5).
+- Terminal/Syntax: font size controls and Phoenix file extension support for better highlighting in files and diffs (thanks to @shekohex).
+- Usage: expanded quota tracking with more providers (including GitHub Copilot) and a provider selector dropdown (thanks to @gsxdsm, @nelsonPires5).
+- Git: improved macOS SSH agent support for smoother private-repo auth (thanks to @shekohex).
+- Web: fixed missing icon when installing the Android PWA (thanks to @nelsonPires5).
+- GitHub: PR description generation supports optional extra context for better summaries (thanks to @nelsonPires5).
+
+
+## [1.6.3] - 2026-02-2
+
+- Web: improved server readiness check to use the `/global/health` endpoint for more reliable startup detection.
+- Web: added login rate limit protection to prevent brute-force attempts on the authentication endpoint (thanks to @Jovines).
+- VSCode: improved server health check with the proper health API endpoint and increased timeout for steadier startup (thanks to @wienans).
+- Settings: dialog no longer persists open/closed state across app restarts.
+
+
+## [1.6.2] - 2026-02-1
+
+- Usage: new multi-provider quota dashboard to monitor API usage across OpenAI, Google, and z.ai (thanks to @nelsonPires5).
+- Settings: now opens in a windowed dialog on desktop with backdrop blur for better focus.
+- Terminal: added tabbed interface to manage multiple terminal sessions per directory.
+- Files: added multi-file tabs on desktop and dropdown selector on mobile (thanks to @nelsonPires5).
+- UI: introduced token-based theming system and 18 themes with light/dark variants; with support for custom user themes from `~/.config/openchamber/themes`.
+- Diff: optimized stacked view with worker-pool processing and lazy DOM rendering for smoother scrolling.
+- Worktrees: workspace path now resolves correctly when using git worktrees (thanks to @nelsonPires5).
+- Projects: fixed directory creation outside workspace in the Add Project modal (thanks to @nelsonPires5).
+
+
+## [1.6.1] - 2026-01-30
+
+- Chat: added Stop button to cancel generation mid-response.
+- Mobile: revamped chat controls on small screens with a unified controls drawer (thanks to @nelsonPires5).
+- UI: update dialog now includes the changelog so you can review what's new before updating.
+- Terminal: added optional on-screen key bar (Esc/Ctrl/arrows/Enter) for easier terminal navigation.
+- Notifications: added "Notify for subtasks" toggle to silence child-session notifications during multi-run (thanks to @Jovines).
+- Reliability: improved event-stream reconnection when the app becomes visible again.
+- Worktrees: starting new worktree sessions now defaults to HEAD when no start point is provided.
+- Git: commit message generation now includes untracked files and handles git diff --no-index comparisons more reliably (thanks to @MrLYC).
+- Desktop: improved macOS window chrome and header spacing, including steadier traffic lights on older macOS versions (thanks to @yulia-ivashko).
+
+
+## [1.6.0] - 2026-01-29
+
+- Chat: added message stall detection with automatic soft resync for more reliable message delivery.
+- Chat: fixed "Load older" button behavior in chat with proper pagination implementation.
+- Git: PR picker now validates local branch existence and includes a refresh action.
+- Git: worktree integration now syncs clean target directories before merging.
+- Diff: fixed memory leak when viewing many modified files; large changesets now lazy-load for smoother performance.
+- VSCode: session activity status now updates reliably even when the webview is hidden.
+- Web: session activity tracking now works consistently across browser tabs.
+- Reliability: plans directory no longer errors when missing.
+
+
+## [1.5.9] - 2026-01-28
+
+- Worktrees: migrated to Opencode SDK worktree implementation; sessions in worktrees are now completely isolated.
+- Git: integrate worktree commits back to a target branch with commit previews and guided conflict handling.
+- Files: toggle markdown preview when viewing files (thanks to @Jovines).
+- Files: open the file viewer in fullscreen for focused review and editing (thanks to @TaylorBeeston).
+- Plans: switch between markdown preview and edit mode in the Plan view.
+- UI: Files, Diff, Git, and Terminal now follow the active session/worktree directory, including new-session drafts.
+- Web: plan lists no longer error when the plans directory is missing.
+
+
+## [1.5.8] - 2026-01-26
+
+- Plans: new Plan/Build mode switching support with dedicated Plan content view with per-session context.
+- GitHub: sign in with multiple accounts and smoother auth flow.
+- Chat/UI: linkable mentions, better wrapping, and markdown/scroll polish in messages.
+- Skills: ClawdHub catalog now pages results and retries transient failures.
+- Diff: fixed Chrome scrolling in All Files layout.
+- Mobile: improved layout for attachments, git, and permissions on small screens (thanks to @nelsonPires5).
+- Web: iOS safe-area support for the PWA header.
+- Activity: added a text-justification setting for activity summaries (thanks to @iyangdianfeng).
+- Reliability: file lists and message sends handle missing directories and transient errors more gracefully.
+
+
+## [1.5.7] - 2026-01-24
+
+- GitHub: PR panel supports fork PR detection by branch name.
+- GitHub: Git tab PR panel can send failed checks/comments to chat with hidden context; added check details dialog with Actions step breakdown.
+- Web: GitHub auth flow fixes.
+
+
+## [1.5.6] - 2026-01-24
+
+- GitHub: connect your account in Settings with device-flow auth to enable GitHub tools.
+- Sessions: start new sessions from GitHub issues with seeded context (title, body, labels, comments).
+- Sessions: start new sessions from GitHub pull requests with PR context baked in (including diffs).
+- Git: manage pull requests in the Git view with AI-generated descriptions, status checks, ready-for-review, and merge actions.
+- Mobile: fixed CommandAutocomplete dropdown scrolling (thanks to @nelsonPires5).
+
+
+## [1.5.5] - 2026-01-23
+
+- Navigation: URLs now sync the active session, tab, settings, and diff state for shareable links and reliable back/forward (thanks to @TaylorBeeston).
+- Settings: agent and command overrides now prefer plural directories while still honoring legacy singular folders.
+- Skills: installs now target plural directories while still recognizing legacy singular folders.
+- Web: push notifications no longer fire when a window is visible, avoiding duplicate alerts.
+- Web: improved push subscription handling across multiple windows for more reliable delivery.
+
+
+## [1.5.4] - 2026-01-22
+
+- Chat: new Apply Patch tool UI with diff preview for patch-based edits.
+- Files: refreshed attachment cards and related file views for clearer context.
+- Settings: manage provider configuration files directly from the UI.
+- UI: updated header and sidebar layout for a cleaner, tighter workspace fit (thanks to @TheRealAshik).
+- Diff: large diffs now lazy-load to avoid freezes (thanks to @Jovines).
+- Web: added Background notifications for PWA.
+- Reliability: connect to external OpenCode servers without auto-start and fixed subagent crashes (thanks to @TaylorBeeston).
+
+
+## [1.5.3] - 2026-01-20
+
+- Files: edit files inline with syntax highlighting, draft protection, and save/discard flow.
+- Files: toggles to show hidden/dotfiles and gitignored entries in file browsers and pickers (thanks to @syntext).
+- Settings: new memory limits controls for session message history.
+- Chat: smoother session switching with more stable scroll anchoring.
+- Chat: new Activity view in collapsed state, now shows latest 6 tools by default.
+- Chat: fixed message copy on Firefox for macOS (thanks to @syntext).
+- Appearance: new corner radius control and restored input bar offset setting (thanks to @TheRealAshik).
+- Git: generated commit messages now auto-pick a gitmoji when enabled (thanks to @TheRealAshik).
+- Performance: faster filesystem/search operations and general stability improvements (thanks to @TheRealAshik).
+
+
+## [1.5.2] - 2026-01-17
+
+- Sessions: added branch picker dialog to start new worktree sessions from local branches (thanks to @nilskroe).
+- Sessions: added project header worktree button, active-session loader, and right-click context menu in the sessions sidebar (thanks to @nilskroe).
+- Sessions: improved worktree delete dialog with linked session details, dirty-change warnings, and optional remote branch removal.
+- Git: added gitmoji picker in commit message composer with cached emoji list (thanks to @TaylorBeeston).
+- Chat: optimized message loading for opening sessions.
+- UI: added one-click diagnostics copy in the About dialog.
+- VSCode: tuned layout breakpoint and server readiness timeout for steadier startup.
+- Reliability: improved OpenCode process cleanup to reduce orphaned servers.
+
+
+## [1.5.1] - 2026-01-16
+
+- Desktop: fixed orphaned OpenCode processes not being cleaned up on restart or exit.
+- Opencode: fixed issue with reloading configuration was killing the app
+
+
+## [1.5.0] - 2026-01-16
+
+- UI: added a new Files tab to browse workspace files directly from the interface.
+- Diff: enhanced the diff viewer with mobile support and the ability to ask the agent for comments on changes.
+- Git Identities: added "default identity" setting with one-click set/unset and automatic local identity detection.
+- VSCode: improved server management to ensure it initializes within the workspace directory with context-aware readiness checks.
+- VSCode: added responsive layout with sessions sidebar + chat side-by-side when wide, compact header, and streamlined settings.
+- Web/VSCode: fixed orphaned OpenCode processes not being cleaned up on restart or exit.
+- Web: the server now automatically resolves and uses an available port if the default is occupied.
+- Stability: fixed heartbeat race condition causing session stalls during long tasks (thanks to @tybradle).
+- Desktop: fixed commands for worktree setup access to PATH.
+
+
+## [1.4.9] - 2026-01-14
+
+- VSCode: added session editor panel to view sessions alongside files.
+- VSCode: improved server connection reliability with multiple URL candidate support.
+- Diff: added stacked/inline diff mode toggle in settings with sidebar file navigation (thanks to @nelsonPires5).
+- Mobile: fixed iOS keyboard safe area padding for home indicator bar (thanks to @Jovines).
+- Upload: increased attachment size limit to 50MB with automatic image compression to 2048px for large files.
+
+
+## [1.4.8] - 2026-01-14
+
+- Git Identities: added token-based authentication support with ~/.git-credentials discovery and import.
+- Settings: consolidated Git settings and added opencode zen model selection for commit generation (thanks to @nelsonPires5).
+- Web Notifications: added configurable native web notifications for assistant completion (thanks to @vio1ator).
+- Chat: sidebar sessions are now automatically sorted by last updated date (thanks to @vio1ator).
+- Chat: fixed edit tool output and added turn duration.
+- UI: todo lists and status indicators now hide automatically when all tasks are completed (thanks to @vio1ator).
+- Reliability: improved project state preservation on validation failures (thanks to @vio1ator) and refined server health monitoring.
+- Stability: added graceful shutdown handling for the server process (thanks to @vio1ator).
+
+
+## [1.4.7] - 2026-01-10
+
+- Skills: added ClawdHub integration as built-in market for skills.
+- Web: fixed issues in terminal
+
+
+## [1.4.6] - 2026-01-09
+
+- VSCode/Web: switch opencode cli management to SDK.
+- Input: removed auto-complete and auto-correction.
+- Shortcuts: switched agent cycling shortcut from Shift + TAB to TAB again.
+- Chat: added question tool support with a rich UI for interaction.
+
+
+## [1.4.5] - 2026-01-08
+
+- Chat: added support for model variants (thinking effort).
+- Shortcuts: Switched agent cycling shortcut from TAB to Shift + TAB.
+- Skills: added autocomplete for skills on "/" when it is not the first character in input.
+- Autocomplete: added scope badges for commands/agents/skills.
+- Compact: changed /summarize command to be /compact and use sdk for compaction.
+- MCP: added ability to dynamically enabled/disabled configured MCP.
+- Web: refactored project adding UI with autocomplete.
+
+
+## [1.4.4] - 2026-01-08
+
+- Agent Manager / Multi Run: select agent per worktree session (thanks to @wienans).
+- Agent Manager / Multi Run: worktree actions to delete group or individual worktrees, or keep only selected one (thanks to @wienans).
+- Agent Manager: added "Copy Worktree Path" action in the more menu (thanks to @wienans).
+- Worktrees: added session creation flow with loading screen, auto-create worktree setting, and setup commands management.
+- Session sidebar: refactoring with unified view for sessions in worktrees.
+- Settings: added ability to create new session in worktree by default
+- Git view: added branch rename for worktree.
+- Chat: fixed IME composition for CJK input to prevent accidental send (thanks to @madebyjun).
+- Projects: added multi-project support with per-project settings for agents/commands/skills.
+- Event stream: improved SSE with heartbeat management, permission bootstrap on connect, and reconnection logic.
+- Tunnel: added QR code and password URL for Cloudflare tunnel (thanks to @martindonadieu).
+- Model selector: fixed dropdowns not responding to viewport size.
+
+
+## [1.4.3] - 2026-01-04
+
+- VS Code extension: added Agent Manager panel to run the same prompt across up to 5 models in parallel (thanks to @wienans).
+- Added permission prompt UI for tools configured with "ask" in opencode.json, showing requested patterns and "Always Allow" options (thanks to @aptdnfapt).
+- Added "Open subAgent session" button on task tool outputs to quickly navigate to child sessions (thanks to @aptdnfapt).
+- VS Code extension: improved activation reliability and error handling.
+
+
+## [1.4.2] - 2026-01-02
+
+- Added timeline dialog (`/timeline` command or Cmd/Ctrl+T) for navigating, reverting, and forking from any point in the conversation (thanks to @aptdnfapt).
+- Added `/undo` and `/redo` commands for reverting and restoring messages in a session (thanks to @aptdnfapt).
+- Added fork button on user messages to create a new session from any point (thanks to @aptdnfapt).
+- Desktop app: keyboard shortcuts now use Cmd on macOS and Ctrl on web/other platforms (thanks to @sakhnyuk).
+- Migrated to OpenCode SDK v2 with improved API types and streaming.
+
+
+## [1.4.1] - 2026-01-02
+
+- Added the ability to select the same model multiple times in multi-agent runs for response comparison.
+- Model selector now includes search and keyboard navigation for faster model selection.
+- Added revert button to all user messages (including first one).
+- Added HEIC image support for file attachments with automatic MIME type normalization for text format files.
+- VS Code extension: added git backend integration for UI to access (thanks to @wienans).
+- VS Code extension: Only show the main Worktree in the Chat Sidebar (thanks to @wienans).
+- Web app: terminal backend now supports a faster Bun-based PTY when Bun is available, with automatic fallback for existing Node-only setups.
+- Terminal: improved terminal performance and stability by switching to the Ghostty-based terminal renderer, while keeping the existing terminal UX and per-directory sessions.
+- Terminal: fixed several issues with terminal session restore and rendering under heavy output, including switching directories and long-running TUI apps.
+
+
+## [1.4.0] - 2026-01-01
+
+- Added the ability to run multiple agents from a single prompt, with each agent working in an isolated worktree.
+- Git view: improved branch publishing by detecting unpublished commits and automatically setting the upstream on first push.
+- Worktrees: new branch creation can start from a chosen base; remote branches are only created when you push.
+- VS Code extension: default location is now the right secondary sidebar in VS Code, and the left activity bar in Cursor/Windsurf; navigation moved into the title bar (thanks to @wienans).
+- Web app: added Cloudflare Quick Tunnel support for simpler remote access (thanks to @wojons and @aptdnfapt).
+- Mobile: improved keyboard/input bar behavior (including Android fixes and better keyboard avoidance) and added an offset setting for curved-screen devices (thanks to @auroraflux).
+- Chat: now shows clearer error messages when agent messages fail.
+- Sidebar: improved readability for sticky headers with a dynamic background.
+
+
+## [1.3.9] - 2025-12-30
+
+ - Added skills management to settings with the ability to create, edit, and delete skills (make sure you have the latest OpenCode version for skills support).
+- Added Skills catalog functionality for discovering and installing skills from external sources.
+- VS Code extension: added right-click context menu with "Add to Context," "Explain," and "Improve Code" actions (thanks to @wienans).
+
+
+## [1.3.8] - 2025-12-29
+
+- Added Intel Mac (x86_64) support for the desktop application (thanks to @rothnic).
+- Build workflow now generates separate builds for Apple Silicon (arm64) and Intel (x86_64) Macs (thanks to @rothnic).
+- Improved dev server HMR by reusing a healthy OpenCode process to avoid zombie instances.
+- Added queued message mode with chips, batching, and idle auto‑send (including attachments).
+- Added queue mode toggle to OpenChamber settings (chat section) with persistence across runtimes.
+- Fixed scroll position persistence for active conversation turns across session switches.
+- Refactored Agents/Commands management with ability to configure project/user scopes.
+
+
+## [1.3.7] - 2025-12-28
+
+- Redesigned Settings as a full-screen view with tabbed navigation.
+- Added mobile-friendly drill-down navigation for settings.
+- ESC key now closes settings; double-ESC abort only works on chat tab without overlays.
+- Added responsive tab labels in settings header (icons only at narrow widths).
+- Improved session activity status handling and message step completion logic.
+- Introduced enchanced VSCode extension settings with dynamic layout based on width.
+
+
+## [1.3.6] - 2025-12-27
+
+- Added the ability to manage (connect/disconnect) providers in settings.
+- Adjusted auto-summarization visuals in chat.
+
+
+## [1.3.5] - 2025-12-26
+
+- Added Nushell support for operations with Opencode CLI.
+- Improved file search with fuzzy matching capabilities.
+- Enhanced mobile responsiveness in chat controls.
+- Fixed workspace switching performance and API health checks.
+- Improved provider loading reliability during workspace switching.
+- Fixed session handling for non-existent worktree directories.
+- Added Discord links in the about section.
+- Added settings for choosing the default model/agent to start with in a new session.
+
+
+## [1.3.4] - 2025-12-25
+
+- Diff view now loads reliably even with large files and slow networks.
+- Fixed getting diffs for worktree files.
+- VS Code extension: improved type checking and editor integration.
+
+
+## [1.3.3] - 2025-12-25
+
+- Updated OpenCode SDK to 1.0.185 across all app versions.
+- VS Code extension: fixed startup, more reliable OpenCode CLI/API management, and stabilized API proxying/streaming.
+- VS Code extension: added an animated loading screen and introduced command for status/debug output.
+- Fixed session activity tracking so it correctly handles transitions through states (including worktree sessions).
+- Fixed directory path handling (including `~` expansion) to prevent invalid paths and related Git/worktree errors.
+- Chat UI: improved turn grouping/activity rendering and fixed message metadata/agent selection propagation.
+- Chat UI: improved agent activity status behavior and reduced image thumbnail sizes for better readability.
+
+
+## [1.3.2] - 2025-12-22
+
+- Fixed new bug session when switching directories
+- Updated Opencode SDK to the latest version
+
+
+## [1.3.1] - 2025-12-22
+
+- New chats no longer create a session until you send your first message.
+- The app opens to a new chat by default.
+- Fixed mobile and VSCode sessions handling
+- Updated app identity with new logo and icons across all platforms.
+
+
+## [1.3.0] - 2025-12-21
+
+- Added revert functionality in chat for user messages.
+- Polished mobile controls in chat view.
+- Updated user message layout/styling.
+- Improved header tab responsiveness.
+- Fixed bugs with new session creation when the VSCode extension initialized for the first time.
+- Adjusted VSCode extension theme mapping and model selection view.
+- Polished file autocomplete experience.
+
+
+## [1.2.9] - 2025-12-20
+
+- Session auto‑cleanup feature with configurable retention for each app version including VSCode extension.
+- Ability to update web package from mobile/PWA view in setting.
+- A lot of different optimization for a long sessions.
+
+
+## [1.2.8] - 2025-12-19
+
+- Introduced update mechanism for web version that doesn't need any cli interaction.
+- Added installation script for web version with package managed detection.
+- Update and restart of web server now support automatic pick-up of previously set parameters like port or password.
+
+
+## [1.2.7] - 2025-12-19
+
+- Comprehensive macOS native menu bar entries.
+- Redesigned directory selection view for web/mobile with improved layout.
+- Improved theme consistency across dropdown menus, selects, and command palette.
+- Introduced keyboard shortcuts help menu and quick actions menu.
+
+
+## [1.2.6] - 2025-12-19
+
+- Added write/create tool preview in permission cards with syntax highlighting.
+- More descriptive assistant status messages with tool-specific and varied idle phrases.
+- Polished Git view layout
+
+
+## [1.2.5] - 2025-12-19
+
+- Polished chat expirience for longer session.
+- Fixed file link from git view to diff.
+- Enhancements to the inactive state management of the desktop app.
+- Redesigned Git tab layout with improved organization.
+- Fixed untracked files in new directories not showing individually.
+- Smoother session rename experience.
+
+
+## [1.2.4] - 2025-12-18
+
+- MacOS app menu entries for Check for update and for creating bug/request in Help section.
+- For Mobile added settings, improved terminal scrolling, fixed app layout positioning.
+
+
+## [1.2.3] - 2025-12-17
+
+- Added image preview support in Diff tab (shows original/modified images instead of base64 code).
+- Improved diff view visuals and alligned style among different widgets.
+- Optimized git polling and background diff+syntax pre-warm for instant Diff tab open.
+- Optomized reloading unaffected diffs.
+
+
+## [1.2.2] - 2025-12-17
+
+- Agent Task tool now renders progressively with live duration and completed sub-tools summary.
+- Unified markdown rendering between assistant messages and tool outputs.
+- Reduced markdown header sizes for better visual balance.
+
+
+## [1.2.1] - 2025-12-16
+
+- Todo task tracking: collapsible status row showing AI's current task and progress.
+- Switched "Detailed" tool output mode to only open the 'task', 'edit', 'multiedit', 'write', 'bash' tools for better performance.
+
+
+## [1.2.0] - 2025-12-15
+
+- Favorite & recent models for quick access in model selection.
+- Tool call expansion settings: collapsed, activity, or detailed modes.
+- Font size & spacing controls (50-200% scaling) in Appearance Settings.
+- Settings page access within VSCode extension.
+Thanks to @theblazehen for contributing these features!
+
+
+## [1.1.6] - 2025-12-15
+
+- Optimized diff view layout with smaller fonts and compact hunk separators.
+- Improved mobile experience: simplified header, better diff file selector.
+- Redesigned password-protected session unlock screen.
+
+
+## [1.1.5] - 2025-12-15
+
+- Enhanced file attachment features performance.
+- Added fuzzy search feature for file mentioning with @ in chat.
+- Optimized input area layout.
+
+
+## [1.1.4] - 2025-12-15
+
+- Flexoki themes for Shiki syntax highlighting for consistency with the app color schema.
+- Enchanced VSCode extension theming with editor themes.
+- Fixed mobile view model/agent selection.
+
+
+## [1.1.3] - 2025-12-14
+
+- Replaced Monaco diff editor with Pierre/diffs for better performance.
+- Added line wrap toggle in diff view with dynamic layout switching (auto-inline when narrow).
+
+
+## [1.1.2] - 2025-12-13
+
+- Moved VS Code extension to activity bar (left sidebar).
+- Added feedback messages for "Restart API Connection" command.
+- Removed redundant VS Code commands.
+- Enhanced UserTextPart styling.
+
+
+## [1.1.1] - 2025-12-13
+
+- Adjusted model/agent selection alignment.
+- Fixed user message rendering issues.
+
+
+## [1.1.0] - 2025-12-13
+
+- Added assistant answer fork flow so users can start a new session from an assistant plan/response with inherited context.
+- Added OpenChamber VS Code extension with editor integration: file picker, click-to-open in tool parts.
+- Improved scroll performance with force flag and RAF placeholder.
+- Added git polling backoff optimization.
+
+
+## [1.0.9] - 2025-12-08
+
+- Added directory picker on first launch to reduce macOS permission prompts.
+- Show changelog in update dialog from current to new version.
+- Improved update dialog UI with inline version display.
+- Added macOS folder access usage descriptions.
+
+
+## [1.0.8] - 2025-12-08
+
+- Added fallback detection for OpenCode CLI in ~/.opencode/bin.
+- Added window focus after app restart/update.
+- Adapted traffic lights position and corner radius for older macOS versions.
+
+
+## [1.0.7] - 2025-12-08
+
+- Optimized Opencode binary detection.
+- Adjusted app update experience.
+
+
+## [1.0.6] - 2025-12-08
+
+- Enhance shell environment detection.
+
+
+## [1.0.5] - 2025-12-07
+
+- Fixed "Load older messages" incorrectly scrolling to bottom.
+- Fixed page refresh getting stuck on splash screen.
+- Disabled devtools and page refresh in production builds.
+
+
+## [1.0.4] - 2025-12-07
+
+- Optimized desktop app start time
+
+
+## [1.0.3] - 2025-12-07
+
+- Updated onboarding UI.
+- Updated sidebar styles.
+
+
+## [1.0.2] - 2025-12-07
+
+- Updated MacOS window design to the latest one.
+
+
+## [1.0.1] - 2025-12-07
+
+- Initial public release of OpenChamber web and desktop packages in a unified monorepo.
+- Added GitHub Actions release pipeline with macOS signing/notarization, npm publish, and release asset uploads.
+- Introduced OpenCode agent chat experience with section-based navigation, theming, and session persistence.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..95b0b7f
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,92 @@
+# Contributing to OpenChamber
+
+## Getting Started
+
+```bash
+git clone https://github.com/btriapitsyn/openchamber.git
+cd openchamber
+bun install
+```
+
+## Dev Scripts
+
+### Web
+
+| Script | Description | Ports |
+|--------|-------------|-------|
+| `bun run dev:web:full` | Build watcher + Express server. No HMR — manual refresh after changes. | `3001` (server + static) |
+| `bun run dev:web:hmr` | Vite dev server + Express API. **Open the Vite URL for HMR**, not the backend. | `5180` (Vite HMR), `3902` (API) |
+
+Both are configurable via env vars: `OPENCHAMBER_PORT`, `OPENCHAMBER_HMR_UI_PORT`, `OPENCHAMBER_HMR_API_PORT`.
+
+### Desktop (Tauri)
+
+```bash
+bun run desktop:dev
+```
+
+Launches Tauri in dev mode with WebView devtools enabled and a distinct dev icon.
+
+### VS Code Extension
+
+```bash
+bun run vscode:dev # Watch mode (extension + webview rebuild on save)
+```
+
+To test in VS Code:
+```bash
+bun run vscode:build && code --extensionDevelopmentPath="$(pwd)/packages/vscode"
+```
+
+### Shared UI (`packages/ui`)
+
+No dev server — this is a source-level library consumed by other packages. During development, `bun run dev` runs type-checking in watch mode.
+
+## Before Submitting
+
+```bash
+bun run type-check # Must pass
+bun run lint # Must pass
+bun run build # Must succeed
+```
+
+## Code Style
+
+- Functional React components only
+- TypeScript strict mode — no `any` without justification
+- Use existing theme colors/typography from `packages/ui/src/lib/theme/` — don't add new ones
+- Components must support light and dark themes
+- Prefer early returns and `if/else`/`switch` over nested ternaries
+- Tailwind v4 for styling; typography via `packages/ui/src/lib/typography.ts`
+
+## Pull Requests
+
+1. Fork and create a branch
+2. Make changes
+3. Run the validation commands above
+4. Submit PR with clear description of what and why
+
+## Project Structure
+
+```
+packages/
+ ui/ Shared React components, hooks, stores, and theme system
+ web/ Web server (Express) + frontend (Vite) + CLI
+ desktop/ Tauri macOS app (thin shell around the web UI)
+ vscode/ VS Code extension (extension host + webview)
+```
+
+See [AGENTS.md](./AGENTS.md) for detailed architecture reference.
+
+## Not a developer?
+
+You can still help:
+
+- Report bugs or UX issues — even "this felt confusing" is valuable feedback
+- Test on different devices, browsers, or OS versions
+- Suggest features or improvements via issues
+- Help others in Discord
+
+## Questions?
+
+Open an [issue](https://github.com/btriapitsyn/openchamber/issues) or ask in [Discord](https://discord.gg/ZYRSdnwwKA).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6a96217
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Bohdan Triapitsyn
+
+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.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..3c129b6
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,32 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If you discover a security vulnerability in OpenChamber, please report it responsibly.
+
+**Email:** [artmore@protonmail.com](mailto:artmore@protonmail.com)
+
+Please include:
+- Description of the vulnerability
+- Steps to reproduce
+- Affected version(s)
+- Potential impact
+
+I'll acknowledge receipt within 48 hours and aim to provide a fix or mitigation as quickly as possible.
+
+**Please do not open public GitHub issues for security vulnerabilities.**
+
+## Scope
+
+OpenChamber handles sensitive context including:
+- UI authentication (password-protected sessions, JWT tokens)
+- Cloudflare tunnel access (remote connectivity)
+- Terminal access (PTY sessions)
+- Git credentials and SSH keys
+- File system operations
+
+Security reports related to any of these areas are especially appreciated.
+
+## Supported Versions
+
+Security fixes are applied to the latest release. There is no LTS or backport policy at this time.
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..2de16d1
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,2346 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "openchamber-monorepo",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.20.0",
+ "@codemirror/commands": "^6.10.1",
+ "@codemirror/lang-cpp": "^6.0.3",
+ "@codemirror/lang-css": "^6.3.1",
+ "@codemirror/lang-go": "^6.0.1",
+ "@codemirror/lang-html": "^6.4.11",
+ "@codemirror/lang-javascript": "^6.2.4",
+ "@codemirror/lang-json": "^6.0.2",
+ "@codemirror/lang-markdown": "^6.5.0",
+ "@codemirror/lang-python": "^6.2.1",
+ "@codemirror/lang-rust": "^6.0.2",
+ "@codemirror/lang-sql": "^6.10.0",
+ "@codemirror/lang-xml": "^6.1.0",
+ "@codemirror/lang-yaml": "^6.1.2",
+ "@codemirror/language": "^6.12.1",
+ "@codemirror/language-data": "^6.5.2",
+ "@codemirror/legacy-modes": "^6.5.2",
+ "@codemirror/lint": "^6.9.2",
+ "@codemirror/search": "^6.6.0",
+ "@codemirror/state": "^6.5.4",
+ "@codemirror/view": "^6.39.13",
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@fontsource/ibm-plex-mono": "^5.2.7",
+ "@fontsource/ibm-plex-sans": "^5.1.1",
+ "@heroui/scroll-shadow": "^2.3.18",
+ "@heroui/system": "^2.4.23",
+ "@heroui/theme": "^2.4.23",
+ "@ibm/plex": "^6.4.1",
+ "@lezer/highlight": "^1.2.3",
+ "@octokit/rest": "^22.0.1",
+ "@opencode-ai/sdk": "^1.2.20",
+ "@pierre/diffs": "1.1.0-beta.13",
+ "@radix-ui/react-collapsible": "^1.1.12",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/react-toggle": "^1.1.10",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@remixicon/react": "^4.7.0",
+ "@streamdown/code": "^1.0.2",
+ "@tanstack/react-virtual": "^3.13.18",
+ "@types/react-syntax-highlighter": "^15.5.13",
+ "adm-zip": "^0.5.16",
+ "beautiful-mermaid": "^1.1.3",
+ "bun-pty": "^0.4.5",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "codemirror-lang-elixir": "^4.0.0",
+ "electron-context-menu": "^4.1.1",
+ "electron-store": "^11.0.2",
+ "express": "^5.1.0",
+ "fuse.js": "^7.1.0",
+ "ghostty-web": "^0.4.0",
+ "heic2any": "^0.0.4",
+ "html-to-image": "^1.11.13",
+ "http-proxy-middleware": "^3.0.5",
+ "jose": "^6.1.3",
+ "jsonc-parser": "^3.3.1",
+ "motion": "^12.23.24",
+ "next-themes": "^0.4.6",
+ "node-pty": "^1.1.0",
+ "openai": "^4.79.0",
+ "prismjs": "^1.30.0",
+ "qrcode": "^1.5.4",
+ "qrcode-terminal": "^0.12.0",
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1",
+ "react-markdown": "^10.1.0",
+ "react-syntax-highlighter": "^15.6.6",
+ "remark-gfm": "^4.0.1",
+ "simple-git": "^3.28.0",
+ "sonner": "^2.0.7",
+ "streamdown": "^2.2.0",
+ "strip-json-comments": "^5.0.3",
+ "tailwind-merge": "^3.3.1",
+ "web-push": "^3.6.7",
+ "ws": "^8.18.3",
+ "yaml": "^2.8.1",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.8",
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.33.0",
+ "@tailwindcss/postcss": "^4.0.0",
+ "@tauri-apps/api": "^2.9.0",
+ "@types/adm-zip": "^0.5.7",
+ "@types/dom-speech-recognition": "^0.0.7",
+ "@types/node": "^24.3.1",
+ "@types/prismjs": "^1.26.6",
+ "@types/qrcode": "^1.5.5",
+ "@types/react": "^19.1.10",
+ "@types/react-dom": "^19.1.7",
+ "@vitejs/plugin-react": "^5.0.0",
+ "autoprefixer": "^10.4.21",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "concurrently": "^9.2.1",
+ "cors": "^2.8.5",
+ "cross-env": "^7.0.3",
+ "electron": "^38.2.0",
+ "electron-builder": "^24.13.3",
+ "eslint": "^9.33.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "globals": "^16.3.0",
+ "nodemon": "^3.1.7",
+ "patch-package": "^8.0.0",
+ "playwright": "^1.58.2",
+ "tailwindcss": "^4.0.0",
+ "tsx": "^4.20.6",
+ "tw-animate-css": "^1.3.8",
+ "typescript": "~5.8.3",
+ "typescript-eslint": "^8.39.1",
+ "vite": "^7.1.2",
+ },
+ },
+ },
+ "overrides": {
+ "@codemirror/language": "6.12.2",
+ "@codemirror/view": "6.39.13",
+ },
+ "packages": {
+ "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="],
+
+ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
+
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
+
+ "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
+
+ "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
+
+ "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
+
+ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
+
+ "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
+
+ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A=="],
+
+ "@codemirror/commands": ["@codemirror/commands@6.10.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q=="],
+
+ "@codemirror/lang-angular": ["@codemirror/lang-angular@0.1.4", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.3" } }, "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g=="],
+
+ "@codemirror/lang-cpp": ["@codemirror/lang-cpp@6.0.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/cpp": "^1.0.0" } }, "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA=="],
+
+ "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="],
+
+ "@codemirror/lang-go": ["@codemirror/lang-go@6.0.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/go": "^1.0.0" } }, "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg=="],
+
+ "@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="],
+
+ "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="],
+
+ "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="],
+
+ "@codemirror/lang-jinja": ["@codemirror/lang-jinja@6.0.0", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0" } }, "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw=="],
+
+ "@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="],
+
+ "@codemirror/lang-less": ["@codemirror/lang-less@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ=="],
+
+ "@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw=="],
+
+ "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw=="],
+
+ "@codemirror/lang-php": ["@codemirror/lang-php@6.0.2", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/php": "^1.0.0" } }, "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA=="],
+
+ "@codemirror/lang-python": ["@codemirror/lang-python@6.2.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.3.2", "@codemirror/language": "^6.8.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/python": "^1.1.4" } }, "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw=="],
+
+ "@codemirror/lang-rust": ["@codemirror/lang-rust@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/rust": "^1.0.0" } }, "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA=="],
+
+ "@codemirror/lang-sass": ["@codemirror/lang-sass@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/sass": "^1.0.0" } }, "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q=="],
+
+ "@codemirror/lang-sql": ["@codemirror/lang-sql@6.10.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w=="],
+
+ "@codemirror/lang-vue": ["@codemirror/lang-vue@0.1.3", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug=="],
+
+ "@codemirror/lang-wast": ["@codemirror/lang-wast@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q=="],
+
+ "@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="],
+
+ "@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw=="],
+
+ "@codemirror/language": ["@codemirror/language@6.12.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="],
+
+ "@codemirror/language-data": ["@codemirror/language-data@6.5.2", "", { "dependencies": { "@codemirror/lang-angular": "^0.1.0", "@codemirror/lang-cpp": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-go": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-less": "^6.0.0", "@codemirror/lang-liquid": "^6.0.0", "@codemirror/lang-markdown": "^6.0.0", "@codemirror/lang-php": "^6.0.0", "@codemirror/lang-python": "^6.0.0", "@codemirror/lang-rust": "^6.0.0", "@codemirror/lang-sass": "^6.0.0", "@codemirror/lang-sql": "^6.0.0", "@codemirror/lang-vue": "^0.1.1", "@codemirror/lang-wast": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.4.0" } }, "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg=="],
+
+ "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="],
+
+ "@codemirror/lint": ["@codemirror/lint@6.9.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA=="],
+
+ "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="],
+
+ "@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="],
+
+ "@codemirror/view": ["@codemirror/view@6.39.13", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw=="],
+
+ "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
+
+ "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
+
+ "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
+
+ "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="],
+
+ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
+
+ "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
+
+ "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
+
+ "@electron/notarize": ["@electron/notarize@2.2.1", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg=="],
+
+ "@electron/osx-sign": ["@electron/osx-sign@1.0.5", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww=="],
+
+ "@electron/universal": ["@electron/universal@1.5.1", "", { "dependencies": { "@electron/asar": "^3.2.1", "@malept/cross-spawn-promise": "^1.1.0", "debug": "^4.3.1", "dir-compare": "^3.0.0", "fs-extra": "^9.0.1", "minimatch": "^3.0.4", "plist": "^3.0.4" } }, "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
+
+ "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
+ "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
+
+ "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
+
+ "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
+
+ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
+
+ "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.7", "", {}, "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w=="],
+
+ "@fontsource/ibm-plex-sans": ["@fontsource/ibm-plex-sans@5.2.8", "", {}, "sha512-eztSXjDhPhcpxNIiGTgMebdLP9qS4rWkysuE1V7c+DjOR0qiezaiDaTwQE7bTnG5HxAY/8M43XKDvs3cYq6ZYQ=="],
+
+ "@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.6", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw=="],
+
+ "@formatjs/fast-memoize": ["@formatjs/fast-memoize@2.2.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ=="],
+
+ "@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@2.11.4", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/icu-skeleton-parser": "1.8.16", "tslib": "^2.8.0" } }, "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw=="],
+
+ "@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@1.8.16", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "tslib": "^2.8.0" } }, "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ=="],
+
+ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="],
+
+ "@heroui/react-rsc-utils": ["@heroui/react-rsc-utils@2.1.9", "", { "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-e77OEjNCmQxE9/pnLDDb93qWkX58/CcgIqdNAczT/zUP+a48NxGq2A2WRimvc1uviwaNL2StriE2DmyZPyYW7Q=="],
+
+ "@heroui/react-utils": ["@heroui/react-utils@2.1.14", "", { "dependencies": { "@heroui/react-rsc-utils": "2.1.9", "@heroui/shared-utils": "2.1.12" }, "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-hhKklYKy9sRH52C9A8P0jWQ79W4MkIvOnKBIuxEMHhigjfracy0o0lMnAUdEsJni4oZKVJYqNGdQl+UVgcmeDA=="],
+
+ "@heroui/scroll-shadow": ["@heroui/scroll-shadow@2.3.19", "", { "dependencies": { "@heroui/react-utils": "2.1.14", "@heroui/shared-utils": "2.1.12", "@heroui/use-data-scroll-overflow": "2.2.13" }, "peerDependencies": { "@heroui/system": ">=2.4.18", "@heroui/theme": ">=2.4.23", "react": ">=18 || >=19.0.0-rc.0", "react-dom": ">=18 || >=19.0.0-rc.0" } }, "sha512-y5mdBlhiITVrFnQTDqEphYj7p5pHqoFSFtVuRRvl9wUec2lMxEpD85uMGsfL8OgQTKIAqGh2s6M360+VJm7ajQ=="],
+
+ "@heroui/shared-utils": ["@heroui/shared-utils@2.1.12", "", {}, "sha512-0iCnxVAkIPtrHQo26Qa5g0UTqMTpugTbClNOrEPsrQuyRAq7Syux998cPwGlneTfB5E5xcU3LiEdA9GUyeK2cQ=="],
+
+ "@heroui/system": ["@heroui/system@2.4.28", "", { "dependencies": { "@heroui/react-utils": "2.1.14", "@heroui/system-rsc": "2.3.24", "@react-aria/i18n": "3.12.16", "@react-aria/overlays": "3.31.2", "@react-aria/utils": "3.33.1" }, "peerDependencies": { "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", "react": ">=18 || >=19.0.0-rc.0", "react-dom": ">=18 || >=19.0.0-rc.0" } }, "sha512-agtiiMFsCLaBrGWWrGBlFrnwi06ym4uouh61c3/m16CHuYfGm0Hx5oJ1mZVF98SJ91mDyU3e6Rv+8mqV9XVgoQ=="],
+
+ "@heroui/system-rsc": ["@heroui/system-rsc@2.3.24", "", { "dependencies": { "@react-types/shared": "3.33.1" }, "peerDependencies": { "@heroui/theme": ">=2.4.24", "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-GEP3hh7j8wE56WdSAIdCcPQOMDdAYk9bSuQpGzXnkYSiSCJKroOyXbRmp9KF2VgpiMXGI0OlO51aj9JWoa+5Tg=="],
+
+ "@heroui/theme": ["@heroui/theme@2.4.26", "", { "dependencies": { "@heroui/shared-utils": "2.1.12", "color": "^4.2.3", "color2k": "^2.0.3", "deepmerge": "4.3.1", "tailwind-merge": "3.4.0", "tailwind-variants": "3.2.2" }, "peerDependencies": { "tailwindcss": ">=4.0.0" } }, "sha512-TYatChq7YyGDcPJytgOMqQwK72qWYb+vIa7mLmX3Cu9+JzFs2VSHu2QqzdhnOHoK0uJr8giDMy0gvJEDuu31vw=="],
+
+ "@heroui/use-data-scroll-overflow": ["@heroui/use-data-scroll-overflow@2.2.13", "", { "dependencies": { "@heroui/shared-utils": "2.1.12" }, "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-zboLXO1pgYdzMUahDcVt5jf+l1jAQ/D9dFqr7AxWLfn6tn7/EgY0f6xIrgWDgJnM0U3hKxVeY13pAeB4AFTqTw=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="],
+
+ "@ibm/telemetry-js": ["@ibm/telemetry-js@1.11.0", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="],
+
+ "@internationalized/date": ["@internationalized/date@3.12.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ=="],
+
+ "@internationalized/message": ["@internationalized/message@3.1.8", "", { "dependencies": { "@swc/helpers": "^0.5.0", "intl-messageformat": "^10.1.0" } }, "sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA=="],
+
+ "@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
+
+ "@internationalized/string": ["@internationalized/string@3.2.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A=="],
+
+ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="],
+
+ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="],
+
+ "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="],
+
+ "@lezer/cpp": ["@lezer/cpp@1.1.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw=="],
+
+ "@lezer/css": ["@lezer/css@1.3.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg=="],
+
+ "@lezer/go": ["@lezer/go@1.0.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ=="],
+
+ "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
+
+ "@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="],
+
+ "@lezer/java": ["@lezer/java@1.1.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw=="],
+
+ "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="],
+
+ "@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="],
+
+ "@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="],
+
+ "@lezer/markdown": ["@lezer/markdown@1.6.3", "", { "dependencies": { "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0" } }, "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw=="],
+
+ "@lezer/php": ["@lezer/php@1.0.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.1.0" } }, "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA=="],
+
+ "@lezer/python": ["@lezer/python@1.1.18", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg=="],
+
+ "@lezer/rust": ["@lezer/rust@1.0.2", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg=="],
+
+ "@lezer/sass": ["@lezer/sass@1.1.0", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ=="],
+
+ "@lezer/xml": ["@lezer/xml@1.0.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww=="],
+
+ "@lezer/yaml": ["@lezer/yaml@1.0.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.0" } }, "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw=="],
+
+ "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@1.1.1", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ=="],
+
+ "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="],
+
+ "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
+
+ "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
+
+ "@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="],
+
+ "@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
+
+ "@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="],
+
+ "@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
+
+ "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="],
+
+ "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
+
+ "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="],
+
+ "@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="],
+
+ "@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
+
+ "@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="],
+
+ "@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
+
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.24", "", {}, "sha512-MQamFkRl4B/3d6oIRLNpkYR2fcwet1V/ffKyOKJXWjtP/CT9PDJMtLpu6olVHjXKQi8zMNltwuMhv1QsNtRlZg=="],
+
+ "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="],
+
+ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
+
+ "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
+
+ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
+
+ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
+
+ "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
+
+ "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
+
+ "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
+
+ "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
+
+ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
+
+ "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
+
+ "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
+
+ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
+
+ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
+
+ "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
+
+ "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
+
+ "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
+
+ "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
+
+ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
+
+ "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
+
+ "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
+
+ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
+
+ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
+
+ "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
+
+ "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
+
+ "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
+
+ "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
+
+ "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
+
+ "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
+
+ "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
+
+ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+ "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
+
+ "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
+
+ "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
+
+ "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
+
+ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
+
+ "@react-aria/focus": ["@react-aria/focus@3.21.5", "", { "dependencies": { "@react-aria/interactions": "^3.27.1", "@react-aria/utils": "^3.33.1", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-V18fwCyf8zqgJdpLQeDU5ZRNd9TeOfBbhLgmX77Zr5ae9XwaoJ1R3SFJG1wCJX60t34AW+aLZSEEK+saQElf3Q=="],
+
+ "@react-aria/i18n": ["@react-aria/i18n@3.12.16", "", { "dependencies": { "@internationalized/date": "^3.12.0", "@internationalized/message": "^3.1.8", "@internationalized/number": "^3.6.5", "@internationalized/string": "^3.2.7", "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.1", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA=="],
+
+ "@react-aria/interactions": ["@react-aria/interactions@3.27.1", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.1", "@react-stately/flags": "^3.1.2", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-M3wLpTTmDflI0QGNK0PJNUaBXXfeBXue8ZxLMngfc1piHNiH4G5lUvWd9W14XVbqrSCVY8i8DfGrNYpyyZu0tw=="],
+
+ "@react-aria/overlays": ["@react-aria/overlays@3.31.2", "", { "dependencies": { "@react-aria/focus": "^3.21.5", "@react-aria/i18n": "^3.12.16", "@react-aria/interactions": "^3.27.1", "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.1", "@react-aria/visually-hidden": "^3.8.31", "@react-stately/flags": "^3.1.2", "@react-stately/overlays": "^3.6.23", "@react-types/button": "^3.15.1", "@react-types/overlays": "^3.9.4", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-78HYI08r6LvcfD34gyv19ArRIjy1qxOKuXl/jYnjLDyQzD4pVb634IQWcm0zt10RdKgyuH6HTqvuDOgZTLet7Q=="],
+
+ "@react-aria/ssr": ["@react-aria/ssr@3.9.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ=="],
+
+ "@react-aria/utils": ["@react-aria/utils@3.33.1", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-stately/flags": "^3.1.2", "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-kIx1Sj6bbAT0pdqCegHuPanR9zrLn5zMRiM7LN12rgRf55S19ptd9g3ncahArifYTRkfEU9VIn+q0HjfMqS9/w=="],
+
+ "@react-aria/visually-hidden": ["@react-aria/visually-hidden@3.8.31", "", { "dependencies": { "@react-aria/interactions": "^3.27.1", "@react-aria/utils": "^3.33.1", "@react-types/shared": "^3.33.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-RTOHHa4n56a9A3criThqFHBifvZoV71+MCkSuNP2cKO662SUWjqKkd0tJt/mBRMEJPkys8K7Eirp6T8Wt5FFRA=="],
+
+ "@react-stately/flags": ["@react-stately/flags@3.1.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg=="],
+
+ "@react-stately/overlays": ["@react-stately/overlays@3.6.23", "", { "dependencies": { "@react-stately/utils": "^3.11.0", "@react-types/overlays": "^3.9.4", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-RzWxots9A6gAzQMP4s8hOAHV7SbJRTFSlQbb6ly1nkWQXacOSZSFNGsKOaS0eIatfNPlNnW4NIkgtGws5UYzfw=="],
+
+ "@react-stately/utils": ["@react-stately/utils@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw=="],
+
+ "@react-types/button": ["@react-types/button@3.15.1", "", { "dependencies": { "@react-types/shared": "^3.33.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-M1HtsKreJkigCnqceuIT22hDJBSStbPimnpmQmsl7SNyqCFY3+DHS7y/Sl3GvqCkzxF7j9UTL0dG38lGQ3K4xQ=="],
+
+ "@react-types/overlays": ["@react-types/overlays@3.9.4", "", { "dependencies": { "@react-types/shared": "^3.33.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-7Z9HaebMFyYBqtv3XVNHEmVkm7AiYviV7gv0c98elEN2Co+eQcKFGvwBM9Gy/lV57zlTqFX1EX/SAqkMEbCLOA=="],
+
+ "@react-types/shared": ["@react-types/shared@3.33.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-oJHtjvLG43VjwemQDadlR5g/8VepK56B/xKO2XORPHt9zlW6IZs3tZrYlvH29BMvoqC7RtE7E5UjgbnbFtDGag=="],
+
+ "@remixicon/react": ["@remixicon/react@4.9.0", "", { "peerDependencies": { "react": ">=18.2.0" } }, "sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
+
+ "@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
+
+ "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
+
+ "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
+
+ "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
+
+ "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
+
+ "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="],
+
+ "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
+
+ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
+
+ "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
+
+ "@streamdown/code": ["@streamdown/code@1.1.0", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-swypCjtE6vv01bnEtPeaw2ew9cbL2nbsLc06HAIK3K6nYXj5WDA8VLR6GEiwdh7HLIPt5dGze+PJ0eJVkqesug=="],
+
+ "@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="],
+
+ "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
+
+ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="],
+
+ "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.21", "", { "dependencies": { "@tanstack/virtual-core": "3.13.21" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-SYXFrmrbPgXBvf+HsOsKhFgqSe4M6B29VHOsX9Jih9TlNkNkDWx0hWMiMLUghMEzyUz772ndzdEeCEBx+3GIZw=="],
+
+ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.21", "", {}, "sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw=="],
+
+ "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
+
+ "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="],
+
+ "@types/adm-zip": ["@types/adm-zip@0.5.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw=="],
+
+ "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+
+ "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
+
+ "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
+
+ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+
+ "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
+
+ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
+
+ "@types/dom-speech-recognition": ["@types/dom-speech-recognition@0.0.7", "", {}, "sha512-NjiUoJbBlKhyufNsMZLSp+pbPNtPAFnR738RCJvtZy/HVQ2TZjmqpMyaeOSMXgxdfZM60nt8QGbtfmQrJAH2sw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
+
+ "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
+
+ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+
+ "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
+
+ "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
+
+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
+
+ "@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="],
+
+ "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
+
+ "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
+
+ "@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="],
+
+ "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
+
+ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
+
+ "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
+
+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+ "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="],
+
+ "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
+
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
+
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
+
+ "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
+
+ "@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="],
+
+ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
+
+ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
+
+ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
+
+ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
+
+ "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
+
+ "app-builder-bin": ["app-builder-bin@4.0.0", "", {}, "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA=="],
+
+ "app-builder-lib": ["app-builder-lib@24.13.3", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/notarize": "2.2.1", "@electron/osx-sign": "1.0.5", "@electron/universal": "1.5.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", "builder-util": "24.13.1", "builder-util-runtime": "9.2.4", "chromium-pickle-js": "^0.2.0", "debug": "^4.3.4", "ejs": "^3.1.8", "electron-publish": "24.13.1", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", "isbinaryfile": "^5.0.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "minimatch": "^5.1.1", "read-config-file": "6.3.2", "sanitize-filename": "^1.6.3", "semver": "^7.3.8", "tar": "^6.1.12", "temp-file": "^3.4.0" }, "peerDependencies": { "dmg-builder": "24.13.3", "electron-builder-squirrel-windows": "24.13.3" } }, "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig=="],
+
+ "archiver": ["archiver@5.3.2", "", { "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", "buffer-crc32": "^0.2.1", "readable-stream": "^3.6.0", "readdir-glob": "^1.1.2", "tar-stream": "^2.2.0", "zip-stream": "^4.1.0" } }, "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw=="],
+
+ "archiver-utils": ["archiver-utils@2.1.0", "", { "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^2.0.0" } }, "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+
+ "asn1.js": ["asn1.js@5.4.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "safer-buffer": "^2.1.0" } }, "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA=="],
+
+ "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
+
+ "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
+
+ "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
+
+ "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
+
+ "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="],
+
+ "autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="],
+
+ "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
+
+ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
+
+ "beautiful-mermaid": ["beautiful-mermaid@1.1.3", "", { "dependencies": { "elkjs": "^0.11.0", "entities": "^7.0.1" } }, "sha512-TItrtrAyHp1vwFfFVYauWGrquouk/6SS21Aq3RsxindSYZODcN4xYrPZD6BiZRU+o5mKJzDPz9MUSMvELdylyg=="],
+
+ "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
+
+ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
+
+ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
+
+ "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
+
+ "bluebird-lst": ["bluebird-lst@1.0.9", "", { "dependencies": { "bluebird": "^3.5.5" } }, "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw=="],
+
+ "bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
+
+ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
+
+ "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
+
+ "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
+
+ "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
+
+ "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
+
+ "buffer-equal": ["buffer-equal@1.0.1", "", {}, "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg=="],
+
+ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
+
+ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+
+ "builder-util": ["builder-util@24.13.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "4.0.0", "bluebird-lst": "^1.0.9", "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0" } }, "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA=="],
+
+ "builder-util-runtime": ["builder-util-runtime@9.2.4", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA=="],
+
+ "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
+
+ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
+ "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
+
+ "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
+
+ "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+ "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001778", "", {}, "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg=="],
+
+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
+ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+ "character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="],
+
+ "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
+
+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+ "character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="],
+
+ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
+
+ "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
+
+ "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
+
+ "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
+
+ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+
+ "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
+
+ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
+ "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
+
+ "codemirror-lang-elixir": ["codemirror-lang-elixir@4.0.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "lezer-elixir": "^1.0.0" } }, "sha512-z6W/XB4b7TZrp9EZYBGVq93vQfvKbff+1iM8YZaVErL0dguBAeLmVRlEv1NuDZHOP1qjJ3NwyibkUkNWn7q9VQ=="],
+
+ "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
+
+ "color2k": ["color2k@2.0.3", "", {}, "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
+
+ "commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
+
+ "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="],
+
+ "compress-commons": ["compress-commons@4.1.2", "", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
+ "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
+
+ "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="],
+
+ "config-file-ts": ["config-file-ts@0.2.6", "", { "dependencies": { "glob": "^10.3.10", "typescript": "^5.3.3" } }, "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w=="],
+
+ "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
+
+ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
+ "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
+
+ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
+
+ "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="],
+
+ "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
+
+ "crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="],
+
+ "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
+
+ "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
+
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
+ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
+
+ "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
+
+ "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
+
+ "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
+
+ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
+
+ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
+
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
+ "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
+
+ "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
+
+ "dir-compare": ["dir-compare@3.3.0", "", { "dependencies": { "buffer-equal": "^1.0.0", "minimatch": "^3.0.4" } }, "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg=="],
+
+ "dmg-builder": ["dmg-builder@24.13.3", "", { "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", "builder-util-runtime": "9.2.4", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ=="],
+
+ "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="],
+
+ "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
+
+ "dotenv": ["dotenv@9.0.2", "", {}, "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg=="],
+
+ "dotenv-expand": ["dotenv-expand@5.1.0", "", {}, "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
+
+ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
+
+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
+
+ "electron": ["electron@38.8.6", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-lyBhcVi9QYAZL6FO6r5twAWAjWnYomo3iVDvrb5SJZlq928BGemHOKG0tPIq41NOLaCu9f3XdEEjMkjQPjprRg=="],
+
+ "electron-builder": ["electron-builder@24.13.3", "", { "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "dmg-builder": "24.13.3", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "read-config-file": "6.3.2", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg=="],
+
+ "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@24.13.3", "", { "dependencies": { "app-builder-lib": "24.13.3", "archiver": "^5.3.1", "builder-util": "24.13.1", "fs-extra": "^10.1.0" } }, "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg=="],
+
+ "electron-context-menu": ["electron-context-menu@4.1.1", "", { "dependencies": { "cli-truncate": "^4.0.0", "electron-dl": "^4.0.0", "electron-is-dev": "^3.0.1" } }, "sha512-kebXha4K2DmR7zrKpO8muzXlUAdhgqQIT7DpPeT4cAKZugwCB/NbuEuT+1SDjOH9vXJYjWK2UQuFvnVw0u0lHg=="],
+
+ "electron-dl": ["electron-dl@4.0.0", "", { "dependencies": { "ext-name": "^5.0.0", "pupa": "^3.1.0", "unused-filename": "^4.0.1" } }, "sha512-USiB9816d2JzKv0LiSbreRfTg5lDk3lWh0vlx/gugCO92ZIJkHVH0UM18EHvKeadErP6Xn4yiTphWzYfbA2Ong=="],
+
+ "electron-is-dev": ["electron-is-dev@3.0.1", "", {}, "sha512-8TjjAh8Ec51hUi3o4TaU0mD3GMTOESi866oRNavj9A3IQJ7pmv+MJVmdZBFGw4GFT36X7bkqnuDNYvkQgvyI8Q=="],
+
+ "electron-publish": ["electron-publish@24.13.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "24.13.1", "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A=="],
+
+ "electron-store": ["electron-store@11.0.2", "", { "dependencies": { "conf": "^15.0.2", "type-fest": "^5.0.1" } }, "sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="],
+
+ "elkjs": ["elkjs@0.11.1", "", {}, "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg=="],
+
+ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
+
+ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
+
+ "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
+
+ "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
+
+ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="],
+
+ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
+
+ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
+
+ "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="],
+
+ "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+ "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
+
+ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
+
+ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+
+ "ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="],
+
+ "ext-name": ["ext-name@5.0.0", "", { "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" } }, "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ=="],
+
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+ "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
+
+ "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
+
+ "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+
+ "fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
+
+ "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="],
+
+ "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
+
+ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
+
+ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+
+ "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
+
+ "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
+
+ "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
+
+ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
+
+ "framer-motion": ["framer-motion@12.35.2", "", { "dependencies": { "motion-dom": "^12.35.2", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA=="],
+
+ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
+ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
+
+ "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
+
+ "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
+
+ "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
+
+ "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+ "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
+
+ "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
+
+ "ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="],
+
+ "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
+
+ "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
+
+ "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
+
+ "hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="],
+
+ "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
+
+ "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],
+
+ "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
+
+ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
+
+ "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
+
+ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+
+ "hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="],
+
+ "heic2any": ["heic2any@0.0.4", "", {}, "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA=="],
+
+ "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
+
+ "highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
+
+ "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
+
+ "html-to-image": ["html-to-image@1.11.13", "", {}, "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="],
+
+ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
+
+ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
+
+ "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
+
+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+
+ "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="],
+
+ "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
+
+ "http-proxy-middleware": ["http-proxy-middleware@3.0.5", "", { "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", "http-proxy": "^1.18.1", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" } }, "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg=="],
+
+ "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
+
+ "http_ece": ["http_ece@1.2.0", "", {}, "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
+
+ "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="],
+
+ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+
+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
+
+ "intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="],
+
+ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
+ "is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
+
+ "is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="],
+
+ "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
+
+ "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
+
+ "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="],
+
+ "is-decimal": ["is-decimal@1.0.4", "", {}, "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="],
+
+ "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-hexadecimal": ["is-hexadecimal@1.0.4", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+ "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
+
+ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
+ "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
+
+ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
+
+ "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
+
+ "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
+
+ "json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
+
+ "json-with-bigint": ["json-with-bigint@3.5.7", "", {}, "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
+
+ "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
+
+ "jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="],
+
+ "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
+
+ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="],
+
+ "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
+
+ "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "lezer-elixir": ["lezer-elixir@1.1.3", "", { "dependencies": { "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.3.0" } }, "sha512-Ymc58/WhxdZS9yEOlnKbF3rdeBdFcPm4OEm26KMqA1Za9vztXi7I5qwGw1KxYmm3Nv0iDHq//EQyBwSEzKG9Mg=="],
+
+ "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
+
+ "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
+
+ "lodash.difference": ["lodash.difference@4.5.0", "", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="],
+
+ "lodash.flatten": ["lodash.flatten@4.4.0", "", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="],
+
+ "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
+
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
+ "lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="],
+
+ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
+
+ "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
+
+ "lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
+
+ "marked": ["marked@17.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ=="],
+
+ "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
+
+ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
+
+ "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
+
+ "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
+
+ "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
+
+ "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
+
+ "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
+
+ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
+
+ "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
+
+ "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
+
+ "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
+
+ "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
+
+ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
+
+ "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
+
+ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
+
+ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
+ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
+ "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
+
+ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
+
+ "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
+
+ "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
+
+ "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
+
+ "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
+
+ "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
+
+ "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
+
+ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
+
+ "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
+
+ "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
+
+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
+
+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
+
+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
+
+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
+
+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
+
+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
+
+ "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
+
+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
+
+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+
+ "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
+
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
+
+ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
+
+ "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
+
+ "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+
+ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
+
+ "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
+
+ "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
+
+ "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
+
+ "motion": ["motion@12.35.2", "", { "dependencies": { "framer-motion": "^12.35.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-8zCi1DkNyU6a/tgEHn/GnnXZDcaMpDHbDOGORY1Rg/6lcNMSOuvwDB3i4hMSOvxqMWArc/vrGaw/Xek1OP69/A=="],
+
+ "motion-dom": ["motion-dom@12.35.2", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg=="],
+
+ "motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
+ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
+
+ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
+
+ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
+
+ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
+
+ "node-pty": ["node-pty@1.1.0", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="],
+
+ "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
+
+ "nodemon": ["nodemon@3.1.14", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw=="],
+
+ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
+
+ "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
+
+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+ "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
+
+ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+ "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
+
+ "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
+
+ "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
+
+ "openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
+
+ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+ "parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
+
+ "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+ "patch-package": ["patch-package@8.0.1", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^10.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.2.4", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+
+ "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
+
+ "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
+
+ "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
+
+ "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
+
+ "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
+ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
+
+ "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
+
+ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
+
+ "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
+
+ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
+
+ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
+ "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="],
+
+ "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="],
+
+ "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
+
+ "qrcode-terminal": ["qrcode-terminal@0.12.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="],
+
+ "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
+
+ "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
+
+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+
+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+
+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
+
+ "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
+
+ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
+
+ "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
+
+ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
+
+ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
+
+ "react-syntax-highlighter": ["react-syntax-highlighter@15.6.6", "", { "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^3.6.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw=="],
+
+ "read-config-file": ["read-config-file@6.3.2", "", { "dependencies": { "config-file-ts": "^0.2.4", "dotenv": "^9.0.2", "dotenv-expand": "^5.1.0", "js-yaml": "^4.1.0", "json5": "^2.2.0", "lazy-val": "^1.0.4" } }, "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q=="],
+
+ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
+
+ "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="],
+
+ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
+
+ "refractor": ["refractor@3.6.0", "", { "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.27.0" } }, "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA=="],
+
+ "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
+
+ "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
+
+ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
+
+ "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="],
+
+ "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
+
+ "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
+
+ "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
+
+ "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
+
+ "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
+
+ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
+
+ "remend": ["remend@1.2.2", "", {}, "sha512-4ZJgIB9EG9fQE41mOJCRHMmnxDTKHWawQoJWZyUbZuj680wVyogu2ihnj8Edqm7vh2mo/TWHyEZpn2kqeDvS7w=="],
+
+ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
+
+ "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
+
+ "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
+
+ "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
+
+ "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
+
+ "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
+
+ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
+
+ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
+ "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="],
+
+ "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
+
+ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
+
+ "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
+
+ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
+
+ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
+
+ "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
+
+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
+
+ "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
+
+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
+ "simple-git": ["simple-git@3.33.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng=="],
+
+ "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
+
+ "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
+
+ "slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="],
+
+ "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
+
+ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
+
+ "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
+
+ "sort-keys": ["sort-keys@1.1.2", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg=="],
+
+ "sort-keys-length": ["sort-keys-length@1.0.1", "", { "dependencies": { "sort-keys": "^1.0.0" } }, "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw=="],
+
+ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
+
+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+
+ "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
+
+ "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
+
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+ "streamdown": ["streamdown@2.4.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.2", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-fRk4HEYNznRLmxoVeT8wsGBwHF6/Yrdey6k+ZrE1Qtp4NyKwm7G/6e2Iw8penY4yLx31TlAHWT5Bsg1weZ9FZg=="],
+
+ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
+
+ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
+
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
+
+ "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="],
+
+ "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
+
+ "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
+
+ "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
+
+ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
+
+ "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
+
+ "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
+
+ "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
+
+ "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
+
+ "tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="],
+
+ "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
+
+ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
+
+ "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
+
+ "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
+
+ "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
+
+ "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+ "touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="],
+
+ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+
+ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
+
+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
+
+ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
+
+ "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
+
+ "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
+
+ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
+
+ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
+ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
+
+ "typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="],
+
+ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
+
+ "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="],
+
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
+ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
+
+ "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
+
+ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
+
+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+ "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
+
+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
+
+ "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
+
+ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+
+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+ "unused-filename": ["unused-filename@4.0.1", "", { "dependencies": { "escape-string-regexp": "^5.0.0", "path-exists": "^5.0.0" } }, "sha512-ZX6U1J04K1FoSUeoX1OicAhw4d0aro2qo+L8RhJkiGTNtBNkd/Fi1Wxoc9HzcVu6HfOzm0si/N15JjxFmD1z6A=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
+
+ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
+
+ "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
+
+ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+
+ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
+ "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
+
+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
+
+ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
+
+ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
+
+ "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
+
+ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
+
+ "web-push": ["web-push@3.6.7", "", { "dependencies": { "asn1.js": "^5.3.0", "http_ece": "1.2.0", "https-proxy-agent": "^7.0.0", "jws": "^4.0.0", "minimist": "^1.2.5" }, "bin": { "web-push": "src/cli.js" } }, "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A=="],
+
+ "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
+
+ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+
+ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+
+ "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+ "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
+
+ "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
+
+ "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
+
+ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+ "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
+ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
+
+ "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+
+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
+ "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="],
+
+ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
+ "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
+
+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+
+ "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
+
+ "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
+
+ "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
+
+ "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
+
+ "@electron/universal/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
+
+ "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
+ "@heroui/theme/tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
+
+ "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+
+ "@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+
+ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
+
+ "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
+
+ "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
+ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
+
+ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "app-builder-lib/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
+
+ "archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
+
+ "archiver-utils/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
+
+ "body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+ "builder-util/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
+
+ "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+
+ "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+
+ "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
+
+ "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
+ "conf/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
+
+ "conf/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
+
+ "decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
+
+ "electron/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
+
+ "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
+
+ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
+
+ "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
+
+ "glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
+
+ "hast-util-from-parse5/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
+
+ "hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
+
+ "hastscript/comma-separated-tokens": ["comma-separated-tokens@1.0.8", "", {}, "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="],
+
+ "hastscript/property-information": ["property-information@5.6.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA=="],
+
+ "hastscript/space-separated-tokens": ["space-separated-tokens@1.1.5", "", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="],
+
+ "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
+
+ "http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
+
+ "iconv-corefoundation/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
+
+ "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
+
+ "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
+
+ "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
+
+ "mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
+
+ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
+
+ "nodemon/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "nodemon/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
+
+ "openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
+
+ "parse-entities/character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="],
+
+ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
+ "path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
+
+ "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
+
+ "raw-body/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+ "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
+
+ "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="],
+
+ "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
+
+ "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
+
+ "sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
+
+ "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "unused-filename/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
+
+ "unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
+
+ "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="],
+
+ "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
+
+ "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
+
+ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+
+ "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+
+ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
+
+ "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+ "app-builder-lib/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "archiver-utils/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
+
+ "archiver-utils/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
+
+ "archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
+
+ "builder-util/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
+
+ "cli-truncate/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+
+ "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+
+ "conf/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+ "electron/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "hast-util-from-parse5/hastscript/hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
+
+ "hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
+
+ "iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
+
+ "lazystream/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
+
+ "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
+
+ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
+
+ "mdast-util-mdx-jsx/parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
+
+ "mdast-util-mdx-jsx/parse-entities/character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
+
+ "mdast-util-mdx-jsx/parse-entities/is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+
+ "mdast-util-mdx-jsx/parse-entities/is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
+
+ "mdast-util-mdx-jsx/parse-entities/is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
+
+ "nodemon/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
+
+ "nodemon/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
+
+ "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
+
+ "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
+
+ "qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
+
+ "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
+
+ "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
+
+ "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "zip-stream/archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+
+ "mdast-util-mdx-jsx/parse-entities/is-alphanumerical/is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
+
+ "nodemon/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
+
+ "qrcode/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
+
+ "qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
+
+ "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
+ }
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..73afbdb
--- /dev/null
+++ b/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..552d212
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { globalIgnores } from 'eslint/config'
+
+export default tseslint.config([
+ globalIgnores(['dist', '.openchamber']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs['recommended-latest'],
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/openchamber.sed b/openchamber.sed
new file mode 100644
index 0000000..a00731e
--- /dev/null
+++ b/openchamber.sed
@@ -0,0 +1,19 @@
+[Version]
+Class=IEXPRESS
+SEDVersion=3
+
+[Options]
+PackageName=OpenChamber.exe
+TargetName=OpenChamber.exe
+Compression=1
+AdvancedInstallOnly=1
+SourceDigest=sha256
+CryptographicAlgorithm=sha256
+RunPostExtractCommand=start.bat
+
+[Source]
+dist-output
+start.bat
+
+[Strings]
+File1=start.bat
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c88eff7
--- /dev/null
+++ b/package.json
@@ -0,0 +1,167 @@
+{
+ "name": "openchamber-monorepo",
+ "version": "1.8.5",
+ "description": "OpenChamber monorepo workspace for web, ui, and desktop runtimes",
+ "private": true,
+ "type": "module",
+ "packageManager": "bun@1.3.5",
+ "workspaces": [],
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "keywords": [
+ "opencode",
+ "ai",
+ "coding",
+ "openchamber",
+ "cli"
+ ],
+ "author": "Bohdan Triapitsyn",
+ "license": "MIT",
+ "scripts": {
+ "dev": "concurrently -n \"server,web,ui\" -c \"cyan,magenta,yellow\" \"bun run --cwd web dev:server:watch\" \"bun run --cwd web build:watch\" \"bun run --cwd ui dev\"",
+ "build:web": "bun run --cwd web build",
+ "build:ui": "bun run --cwd ui build",
+ "package": "bun run build:web && bunx shx rm -rf dist-output && bunx shx mkdir dist-output && bunx shx cp -r web/dist dist-output/ && bunx shx cp -r web/server dist-output/ && bunx shx cp -r web/bin dist-output/ && bunx shx cp web/package.json dist-output/",
+ "type-check:web": "bun run --cwd web type-check",
+ "type-check:ui": "bun run --cwd ui type-check",
+ "lint:web": "bun run --cwd web lint",
+ "lint:ui": "bun run --cwd ui lint",
+ "dev:web": "bun run --cwd web build:watch",
+ "dev:web:server": "bun run --cwd web dev:server:watch",
+ "start:web": "bun run --cwd web start",
+ "pack:web": "bun pm pack --cwd web",
+ "icons:sprite": "node scripts/generate-file-type-sprite.mjs",
+ "version:bump": "node scripts/bump-version.mjs",
+ "release:prepare": "bun run build:web && bun run type-check:web && bun run lint:web",
+ "release:test": "./scripts/test-release-build.sh",
+ "release:test:intel": "./scripts/test-release-build.sh x86_64",
+ "release:test:arm": "./scripts/test-release-build.sh aarch64"
+ },
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.20.0",
+ "@codemirror/commands": "^6.10.1",
+ "@codemirror/lang-css": "^6.3.1",
+ "@codemirror/lang-html": "^6.4.11",
+ "@codemirror/lang-javascript": "^6.2.4",
+ "@codemirror/lang-json": "^6.0.2",
+ "@codemirror/lang-markdown": "^6.5.0",
+ "@codemirror/lang-python": "^6.2.1",
+ "@codemirror/lang-rust": "^6.0.2",
+ "@codemirror/lang-cpp": "^6.0.3",
+ "@codemirror/lang-go": "^6.0.1",
+ "@codemirror/lang-sql": "^6.10.0",
+ "@codemirror/lang-xml": "^6.1.0",
+ "@codemirror/lang-yaml": "^6.1.2",
+ "@codemirror/language": "^6.12.1",
+ "@codemirror/language-data": "^6.5.2",
+ "@codemirror/legacy-modes": "^6.5.2",
+ "@codemirror/lint": "^6.9.2",
+ "@codemirror/search": "^6.6.0",
+ "@codemirror/state": "^6.5.4",
+ "@codemirror/view": "^6.39.13",
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@fontsource/ibm-plex-mono": "^5.2.7",
+ "@fontsource/ibm-plex-sans": "^5.1.1",
+ "@heroui/scroll-shadow": "^2.3.18",
+ "@heroui/system": "^2.4.23",
+ "@heroui/theme": "^2.4.23",
+ "@ibm/plex": "^6.4.1",
+ "@lezer/highlight": "^1.2.3",
+ "@octokit/rest": "^22.0.1",
+ "@opencode-ai/sdk": "^1.2.20",
+ "@pierre/diffs": "1.1.0-beta.13",
+ "@radix-ui/react-collapsible": "^1.1.12",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/react-toggle": "^1.1.10",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@remixicon/react": "^4.7.0",
+ "@streamdown/code": "^1.0.2",
+ "@tanstack/react-virtual": "^3.13.18",
+ "@types/react-syntax-highlighter": "^15.5.13",
+ "beautiful-mermaid": "^1.1.3",
+ "bun-pty": "^0.4.5",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "codemirror-lang-elixir": "^4.0.0",
+ "electron-context-menu": "^4.1.1",
+ "electron-store": "^11.0.2",
+ "express": "^5.1.0",
+ "fuse.js": "^7.1.0",
+ "ghostty-web": "^0.4.0",
+ "heic2any": "^0.0.4",
+ "html-to-image": "^1.11.13",
+ "http-proxy-middleware": "^3.0.5",
+ "motion": "^12.23.24",
+ "next-themes": "^0.4.6",
+ "node-pty": "^1.1.0",
+ "prismjs": "^1.30.0",
+ "qrcode": "^1.5.4",
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1",
+ "react-markdown": "^10.1.0",
+ "react-syntax-highlighter": "^15.6.6",
+ "remark-gfm": "^4.0.1",
+ "simple-git": "^3.28.0",
+ "sonner": "^2.0.7",
+ "streamdown": "^2.2.0",
+ "strip-json-comments": "^5.0.3",
+ "tailwind-merge": "^3.3.1",
+ "yaml": "^2.8.1",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.8",
+ "adm-zip": "^0.5.16",
+ "jose": "^6.1.3",
+ "jsonc-parser": "^3.3.1",
+ "openai": "^4.79.0",
+ "qrcode-terminal": "^0.12.0",
+ "web-push": "^3.6.7",
+ "ws": "^8.18.3"
+ },
+ "overrides": {
+ "@codemirror/language": "6.12.2",
+ "@codemirror/view": "6.39.13"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.33.0",
+ "@tailwindcss/postcss": "^4.0.0",
+ "@tauri-apps/api": "^2.9.0",
+ "@types/dom-speech-recognition": "^0.0.7",
+ "@types/node": "^24.3.1",
+ "@types/prismjs": "^1.26.6",
+ "@types/qrcode": "^1.5.5",
+ "@types/react": "^19.1.10",
+ "@types/react-dom": "^19.1.7",
+ "@vitejs/plugin-react": "^5.0.0",
+ "autoprefixer": "^10.4.21",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "concurrently": "^9.2.1",
+ "cors": "^2.8.5",
+ "cross-env": "^7.0.3",
+ "electron": "^38.2.0",
+ "electron-builder": "^24.13.3",
+ "eslint": "^9.33.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "globals": "^16.3.0",
+ "nodemon": "^3.1.7",
+ "patch-package": "^8.0.0",
+ "playwright": "^1.58.2",
+ "tailwindcss": "^4.0.0",
+ "tsx": "^4.20.6",
+ "tw-animate-css": "^1.3.8",
+ "typescript": "~5.8.3",
+ "typescript-eslint": "^8.39.1",
+ "vite": "^7.1.2",
+ "@types/adm-zip": "^0.5.7"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..db3c3d1
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+}
\ No newline at end of file
diff --git a/run.bat b/run.bat
new file mode 100644
index 0000000..50eec2f
--- /dev/null
+++ b/run.bat
@@ -0,0 +1,23 @@
+@echo off
+echo Starting OpenCode...
+start "OpenCode" cmd /c "opencode serve"
+
+echo Waiting for OpenCode to start...
+:wait_loop
+curl -s http://localhost:4096/health >nul 2>&1
+if %errorlevel% neq 0 (
+ timeout /t 1 >nul
+ goto wait_loop
+)
+echo OpenCode started on port 4096
+
+echo.
+echo Starting OpenChamber...
+start "OpenChamber" cmd /c "bun run start:web"
+
+echo.
+echo ========================================
+echo OpenChamber should be ready at:
+echo http://localhost:3000
+echo ========================================
+pause
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..552bb0a
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./ui/tsconfig.json" },
+ { "path": "./web/tsconfig.json" }
+ ],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "moduleResolution": "bundler",
+ "moduleDetection": "force",
+ "verbatimModuleSyntax": true,
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "paths": {
+ "@openchamber/ui/*": ["./ui/src/*"],
+ "@openchamber/web/*": ["./web/src/*"]
+ }
+ }
+}
diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 0000000..de43905
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,113 @@
+{
+ "name": "@openchamber/ui",
+ "version": "1.8.5",
+ "private": true,
+ "type": "module",
+ "main": "src/main.tsx",
+ "scripts": {
+ "dev": "tsc --noEmit --watch",
+ "build": "tsc --noEmit",
+ "type-check": "tsc --noEmit",
+ "lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../eslint.config.js"
+ },
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.20.0",
+ "@codemirror/commands": "^6.10.1",
+ "@codemirror/lang-cpp": "^6.0.3",
+ "@codemirror/lang-css": "^6.3.1",
+ "@codemirror/lang-go": "^6.0.1",
+ "@codemirror/lang-html": "^6.4.11",
+ "@codemirror/lang-javascript": "^6.2.4",
+ "@codemirror/lang-json": "^6.0.2",
+ "@codemirror/lang-markdown": "^6.5.0",
+ "@codemirror/lang-python": "^6.2.1",
+ "@codemirror/lang-rust": "^6.0.2",
+ "@codemirror/lang-sql": "^6.10.0",
+ "@codemirror/lang-xml": "^6.1.0",
+ "@codemirror/lang-yaml": "^6.1.2",
+ "@codemirror/language": "^6.12.1",
+ "@codemirror/language-data": "^6.5.2",
+ "@codemirror/legacy-modes": "^6.5.2",
+ "@codemirror/lint": "^6.9.2",
+ "@codemirror/search": "^6.6.0",
+ "@codemirror/state": "^6.5.4",
+ "@codemirror/view": "^6.39.13",
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@fontsource/ibm-plex-mono": "^5.2.7",
+ "@fontsource/ibm-plex-sans": "^5.1.1",
+ "@ibm/plex": "^6.4.1",
+ "@lezer/highlight": "^1.2.3",
+ "@opencode-ai/sdk": "^1.2.20",
+ "@pierre/diffs": "1.1.0-beta.13",
+ "@radix-ui/react-collapsible": "^1.1.12",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/react-toggle": "^1.1.10",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@remixicon/react": "^4.7.0",
+ "@streamdown/code": "^1.0.2",
+ "@tanstack/react-virtual": "^3.13.18",
+ "@types/react-syntax-highlighter": "^15.5.13",
+ "beautiful-mermaid": "^1.1.3",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "codemirror-lang-elixir": "^4.0.0",
+ "express": "^5.1.0",
+ "fuse.js": "^7.1.0",
+ "ghostty-web": "^0.4.0",
+ "heic2any": "^0.0.4",
+ "html-to-image": "^1.11.13",
+ "http-proxy-middleware": "^3.0.5",
+ "motion": "^12.23.24",
+ "next-themes": "^0.4.6",
+ "prismjs": "^1.30.0",
+ "qrcode": "^1.5.4",
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1",
+ "react-syntax-highlighter": "^15.6.6",
+ "simple-git": "^3.28.0",
+ "sonner": "^2.0.7",
+ "streamdown": "^2.2.0",
+ "strip-json-comments": "^5.0.3",
+ "tailwind-merge": "^3.3.1",
+ "yaml": "^2.8.1",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.8"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.33.0",
+ "@tailwindcss/postcss": "^4.0.0",
+ "@tauri-apps/api": "^2.9.0",
+ "@types/node": "^24.3.1",
+ "@types/prismjs": "^1.26.6",
+ "@types/qrcode": "^1.5.5",
+ "@types/react": "^19.1.10",
+ "@types/react-dom": "^19.1.7",
+ "@vitejs/plugin-react": "^5.0.0",
+ "autoprefixer": "^10.4.21",
+ "concurrently": "^9.2.1",
+ "cors": "^2.8.5",
+ "cross-env": "^7.0.3",
+ "electron": "^38.2.0",
+ "electron-builder": "^24.13.3",
+ "eslint": "^9.33.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "globals": "^16.3.0",
+ "nodemon": "^3.1.7",
+ "tailwindcss": "^4.0.0",
+ "tsx": "^4.20.6",
+ "tw-animate-css": "^1.3.8",
+ "typescript": "~5.8.3",
+ "typescript-eslint": "^8.39.1",
+ "vite": "^7.1.2"
+ }
+}
diff --git a/ui/src/App.css b/ui/src/App.css
new file mode 100644
index 0000000..6ee5bff
--- /dev/null
+++ b/ui/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: none;
+}
+.logo.react:hover {
+ filter: none;
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
new file mode 100644
index 0000000..1e0c440
--- /dev/null
+++ b/ui/src/App.tsx
@@ -0,0 +1,548 @@
+import React from 'react';
+import { MainLayout } from '@/components/layout/MainLayout';
+import { VSCodeLayout } from '@/components/layout/VSCodeLayout';
+import { AgentManagerView } from '@/components/views/agent-manager';
+import { ChatView } from '@/components/views';
+import { FireworksProvider } from '@/contexts/FireworksContext';
+import { Toaster } from '@/components/ui/sonner';
+import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel';
+import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
+import { useEventStream } from '@/hooks/useEventStream';
+import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
+import { useMenuActions } from '@/hooks/useMenuActions';
+import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap';
+import { useServerSessionStatus } from '@/hooks/useServerSessionStatus';
+import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
+import { useQueuedMessageAutoSend } from '@/hooks/useQueuedMessageAutoSend';
+import { useRouter } from '@/hooks/useRouter';
+import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
+import { useWindowTitle } from '@/hooks/useWindowTitle';
+import { useGitHubPrBackgroundTracking } from '@/hooks/useGitHubPrBackgroundTracking';
+import { GitPollingProvider } from '@/hooks/useGitPolling';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { hasModifier } from '@/lib/utils';
+import { isDesktopLocalOriginActive, isDesktopShell } from '@/lib/desktop';
+import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useDirectoryStore } from '@/stores/useDirectoryStore';
+import { opencodeClient } from '@/lib/opencode/client';
+import { useFontPreferences } from '@/hooks/useFontPreferences';
+import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions';
+import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay';
+import { AboutDialog } from '@/components/ui/AboutDialog';
+import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
+import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
+import { useUIStore } from '@/stores/useUIStore';
+import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
+import type { RuntimeAPIs } from '@/lib/api/types';
+import { TooltipProvider } from '@/components/ui/tooltip';
+
+const CLI_MISSING_ERROR_REGEX =
+ /ENOENT|spawn\s+opencode|Unable\s+to\s+locate\s+the\s+opencode\s+CLI|OpenCode\s+CLI\s+not\s+found|opencode(\.exe)?\s+not\s+found|opencode(\.exe)?:\s*command\s+not\s+found|not\s+recognized\s+as\s+an\s+internal\s+or\s+external\s+command|env:\s*['"]?(node|bun)['"]?:\s*No\s+such\s+file\s+or\s+directory|(node|bun):\s*No\s+such\s+file\s+or\s+directory/i;
+const CLI_ONBOARDING_HEALTH_POLL_MS = 1500;
+
+const AboutDialogWrapper: React.FC = () => {
+ const { isAboutDialogOpen, setAboutDialogOpen } = useUIStore();
+ return (
+
+ );
+};
+
+type AppProps = {
+ apis: RuntimeAPIs;
+};
+
+type EmbeddedSessionChatConfig = {
+ sessionId: string;
+ directory: string | null;
+};
+
+type EmbeddedVisibilityPayload = {
+ visible?: unknown;
+};
+
+const readEmbeddedSessionChatConfig = (): EmbeddedSessionChatConfig | null => {
+ if (typeof window === 'undefined') {
+ return null;
+ }
+
+ const params = new URLSearchParams(window.location.search);
+ if (params.get('ocPanel') !== 'session-chat') {
+ return null;
+ }
+
+ const sessionIdRaw = params.get('sessionId');
+ const sessionId = typeof sessionIdRaw === 'string' ? sessionIdRaw.trim() : '';
+ if (!sessionId) {
+ return null;
+ }
+
+ const directoryRaw = params.get('directory');
+ const directory = typeof directoryRaw === 'string' && directoryRaw.trim().length > 0
+ ? directoryRaw.trim()
+ : null;
+
+ return {
+ sessionId,
+ directory,
+ };
+};
+
+function App({ apis }: AppProps) {
+ const { initializeApp, isInitialized, isConnected } = useConfigStore();
+ const providersCount = useConfigStore((state) => state.providers.length);
+ const agentsCount = useConfigStore((state) => state.agents.length);
+ const loadProviders = useConfigStore((state) => state.loadProviders);
+ const loadAgents = useConfigStore((state) => state.loadAgents);
+ const { error, clearError, loadSessions } = useSessionStore();
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+ const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
+ const sessions = useSessionStore((state) => state.sessions);
+ const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
+ const setDirectory = useDirectoryStore((state) => state.setDirectory);
+ const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory);
+ const [showMemoryDebug, setShowMemoryDebug] = React.useState(false);
+ const { uiFont, monoFont } = useFontPreferences();
+ const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
+ const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState(() => apis.runtime.isVSCode);
+ const [showCliOnboarding, setShowCliOnboarding] = React.useState(false);
+ const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true);
+ const appReadyDispatchedRef = React.useRef(false);
+ const embeddedSessionChat = React.useMemo(() => readEmbeddedSessionChatConfig(), []);
+ const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible;
+
+ React.useEffect(() => {
+ setIsVSCodeRuntime(apis.runtime.isVSCode);
+ }, [apis.runtime.isVSCode]);
+
+ React.useEffect(() => {
+ registerRuntimeAPIs(apis);
+ return () => registerRuntimeAPIs(null);
+ }, [apis]);
+
+ React.useEffect(() => {
+ if (embeddedSessionChat) {
+ return;
+ }
+
+ void refreshGitHubAuthStatus(apis.github, { force: true });
+ }, [apis.github, embeddedSessionChat, refreshGitHubAuthStatus]);
+
+ useGitHubPrBackgroundTracking(embeddedBackgroundWorkEnabled ? apis.github : undefined, apis.git);
+
+ React.useEffect(() => {
+ if (typeof document === 'undefined') {
+ return;
+ }
+ const root = document.documentElement;
+ const uiStack = UI_FONT_OPTION_MAP[uiFont]?.stack ?? UI_FONT_OPTION_MAP[DEFAULT_UI_FONT].stack;
+ const monoStack = CODE_FONT_OPTION_MAP[monoFont]?.stack ?? CODE_FONT_OPTION_MAP[DEFAULT_MONO_FONT].stack;
+
+ root.style.setProperty('--font-sans', uiStack);
+ root.style.setProperty('--font-heading', uiStack);
+ root.style.setProperty('--font-family-sans', uiStack);
+ root.style.setProperty('--font-mono', monoStack);
+ root.style.setProperty('--font-family-mono', monoStack);
+ root.style.setProperty('--ui-regular-font-weight', '400');
+
+ if (document.body) {
+ document.body.style.fontFamily = uiStack;
+ }
+ }, [uiFont, monoFont]);
+
+ React.useEffect(() => {
+ if (isInitialized) {
+ const hideInitialLoading = () => {
+ const loadingElement = document.getElementById('initial-loading');
+ if (loadingElement) {
+ loadingElement.classList.add('fade-out');
+
+ setTimeout(() => {
+ loadingElement.remove();
+ }, 300);
+ }
+ };
+
+ const timer = setTimeout(hideInitialLoading, 150);
+ return () => clearTimeout(timer);
+ }
+ }, [isInitialized]);
+
+ React.useEffect(() => {
+ const fallbackTimer = setTimeout(() => {
+ const loadingElement = document.getElementById('initial-loading');
+ if (loadingElement && !isInitialized) {
+ loadingElement.classList.add('fade-out');
+ setTimeout(() => {
+ loadingElement.remove();
+ }, 300);
+ }
+ }, 5000);
+
+ return () => clearTimeout(fallbackTimer);
+ }, [isInitialized]);
+
+ React.useEffect(() => {
+ const init = async () => {
+ // VS Code runtime bootstraps config + sessions after the managed OpenCode instance reports "connected".
+ // Doing the default initialization here can race with startup and lead to one-shot failures.
+ if (isVSCodeRuntime) {
+ return;
+ }
+ await initializeApp();
+ };
+
+ init();
+ }, [initializeApp, isVSCodeRuntime]);
+
+ const startupRecoveryInProgressRef = React.useRef(false);
+ const startupRecoveryLastAttemptRef = React.useRef(0);
+
+ React.useEffect(() => {
+ if (isVSCodeRuntime) {
+ return;
+ }
+ if (!isConnected) {
+ return;
+ }
+ if (providersCount > 0 && agentsCount > 0) {
+ return;
+ }
+ if (startupRecoveryInProgressRef.current) {
+ return;
+ }
+
+ const now = Date.now();
+ if (now - startupRecoveryLastAttemptRef.current < 750) {
+ return;
+ }
+
+ startupRecoveryLastAttemptRef.current = now;
+ startupRecoveryInProgressRef.current = true;
+
+ const repair = async () => {
+ try {
+ if (providersCount === 0) {
+ await loadProviders();
+ }
+ if (agentsCount === 0) {
+ await loadAgents();
+ }
+ } catch {
+ // Keep UI responsive; we'll retry on next cycle.
+ } finally {
+ startupRecoveryInProgressRef.current = false;
+ }
+ };
+
+ void repair();
+ }, [agentsCount, isConnected, isVSCodeRuntime, loadAgents, loadProviders, providersCount]);
+
+ React.useEffect(() => {
+ if (isSwitchingDirectory) {
+ return;
+ }
+
+ const syncDirectoryAndSessions = async () => {
+ // VS Code runtime loads sessions via VSCodeLayout bootstrap to avoid startup races.
+ if (isVSCodeRuntime) {
+ return;
+ }
+
+ if (!isConnected) {
+ return;
+ }
+ opencodeClient.setDirectory(currentDirectory);
+
+ await loadSessions();
+ };
+
+ syncDirectoryAndSessions();
+ }, [currentDirectory, isSwitchingDirectory, loadSessions, isConnected, isVSCodeRuntime]);
+
+ React.useEffect(() => {
+ if (!embeddedSessionChat || typeof window === 'undefined') {
+ return;
+ }
+
+ const applyVisibility = (payload?: EmbeddedVisibilityPayload) => {
+ const nextVisible = payload?.visible === true;
+ setIsEmbeddedVisible(nextVisible);
+ };
+
+ const handleMessage = (event: MessageEvent) => {
+ if (event.origin !== window.location.origin) {
+ return;
+ }
+
+ const data = event.data as { type?: unknown; payload?: EmbeddedVisibilityPayload };
+ if (data?.type !== 'openchamber:embedded-visibility') {
+ return;
+ }
+
+ applyVisibility(data.payload);
+ };
+
+ const scopedWindow = window as unknown as {
+ __openchamberSetEmbeddedVisibility?: (payload?: EmbeddedVisibilityPayload) => void;
+ };
+
+ scopedWindow.__openchamberSetEmbeddedVisibility = applyVisibility;
+ window.addEventListener('message', handleMessage);
+
+ return () => {
+ window.removeEventListener('message', handleMessage);
+ if (scopedWindow.__openchamberSetEmbeddedVisibility === applyVisibility) {
+ delete scopedWindow.__openchamberSetEmbeddedVisibility;
+ }
+ };
+ }, [embeddedSessionChat]);
+
+ React.useEffect(() => {
+ if (!embeddedSessionChat?.directory || isVSCodeRuntime) {
+ return;
+ }
+
+ if (currentDirectory === embeddedSessionChat.directory) {
+ return;
+ }
+
+ setDirectory(embeddedSessionChat.directory, { showOverlay: false });
+ }, [currentDirectory, embeddedSessionChat, isVSCodeRuntime, setDirectory]);
+
+ React.useEffect(() => {
+ if (!embeddedSessionChat || isVSCodeRuntime) {
+ return;
+ }
+
+ if (currentSessionId === embeddedSessionChat.sessionId) {
+ return;
+ }
+
+ if (!sessions.some((session) => session.id === embeddedSessionChat.sessionId)) {
+ return;
+ }
+
+ void setCurrentSession(embeddedSessionChat.sessionId);
+ }, [currentSessionId, embeddedSessionChat, isVSCodeRuntime, sessions, setCurrentSession]);
+
+ React.useEffect(() => {
+ if (!embeddedSessionChat || typeof window === 'undefined') {
+ return;
+ }
+
+ const handleStorage = (event: StorageEvent) => {
+ if (event.storageArea !== window.localStorage) {
+ return;
+ }
+
+ if (event.key !== 'ui-store') {
+ return;
+ }
+
+ void useUIStore.persist.rehydrate();
+ };
+
+ window.addEventListener('storage', handleStorage);
+ return () => {
+ window.removeEventListener('storage', handleStorage);
+ };
+ }, [embeddedSessionChat]);
+
+ React.useEffect(() => {
+ if (typeof window === 'undefined') return;
+ if (!isInitialized || isSwitchingDirectory) return;
+ if (appReadyDispatchedRef.current) return;
+ appReadyDispatchedRef.current = true;
+ (window as unknown as { __openchamberAppReady?: boolean }).__openchamberAppReady = true;
+ window.dispatchEvent(new Event('openchamber:app-ready'));
+ }, [isInitialized, isSwitchingDirectory]);
+
+ useEventStream({ enabled: embeddedBackgroundWorkEnabled });
+
+ // Server-authoritative session status polling
+ // Replaces SSE-dependent status updates with reliable HTTP polling
+ useServerSessionStatus({ enabled: embeddedBackgroundWorkEnabled });
+
+ usePushVisibilityBeacon({ enabled: embeddedBackgroundWorkEnabled });
+
+ useWindowTitle();
+
+ useRouter();
+
+ useKeyboardShortcuts();
+
+ const handleToggleMemoryDebug = React.useCallback(() => {
+ setShowMemoryDebug(prev => !prev);
+ }, []);
+
+ useMenuActions(handleToggleMemoryDebug);
+
+ useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled });
+ useSessionAutoCleanup({ enabled: embeddedBackgroundWorkEnabled });
+ useQueuedMessageAutoSend({ enabled: embeddedBackgroundWorkEnabled });
+
+ React.useEffect(() => {
+ if (embeddedSessionChat) {
+ return;
+ }
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (hasModifier(e) && e.shiftKey && e.key === 'D') {
+ e.preventDefault();
+ setShowMemoryDebug(prev => !prev);
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [embeddedSessionChat]);
+
+ React.useEffect(() => {
+ if (embeddedSessionChat) {
+ return;
+ }
+
+ if (error) {
+
+ setTimeout(() => clearError(), 5000);
+ }
+ }, [clearError, embeddedSessionChat, error]);
+
+ React.useEffect(() => {
+ if (embeddedSessionChat) {
+ return;
+ }
+
+ if (!isDesktopShell() || !isDesktopLocalOriginActive()) {
+ return;
+ }
+
+ let cancelled = false;
+ const run = async () => {
+ const res = await fetch('/health', { method: 'GET' }).catch(() => null);
+ if (!res || !res.ok || cancelled) return;
+ const data = (await res.json().catch(() => null)) as null | {
+ openCodeRunning?: unknown;
+ isOpenCodeReady?: unknown;
+ opencodeBinaryResolved?: unknown;
+ lastOpenCodeError?: unknown;
+ };
+ if (!data || cancelled) return;
+ const openCodeRunning = data.openCodeRunning === true;
+ const isOpenCodeReady = data.isOpenCodeReady === true;
+ const resolvedBinary = typeof data.opencodeBinaryResolved === 'string' ? data.opencodeBinaryResolved.trim() : '';
+ const hasResolvedBinary = resolvedBinary.length > 0;
+ const err = typeof data.lastOpenCodeError === 'string' ? data.lastOpenCodeError : '';
+ const cliMissing =
+ !openCodeRunning &&
+ (CLI_MISSING_ERROR_REGEX.test(err) || (!hasResolvedBinary && !isOpenCodeReady));
+ setShowCliOnboarding(cliMissing);
+ };
+
+ void run();
+ const interval = window.setInterval(() => {
+ void run();
+ }, CLI_ONBOARDING_HEALTH_POLL_MS);
+
+ return () => {
+ cancelled = true;
+ window.clearInterval(interval);
+ };
+ }, [embeddedSessionChat]);
+
+ const handleCliAvailable = React.useCallback(() => {
+ setShowCliOnboarding(false);
+ window.location.reload();
+ }, []);
+
+ if (showCliOnboarding) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (embeddedSessionChat) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ // VS Code runtime - simplified layout without git/terminal views
+ if (isVSCodeRuntime) {
+ // Check if this is the Agent Manager panel
+ const panelType = typeof window !== 'undefined'
+ ? (window as { __OPENCHAMBER_PANEL_TYPE__?: 'chat' | 'agentManager' }).__OPENCHAMBER_PANEL_TYPE__
+ : 'chat';
+
+ if (panelType === 'agentManager') {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {showMemoryDebug && (
+
setShowMemoryDebug(false)} />
+ )}
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/ui/src/assets/icons/file-types/3d.svg b/ui/src/assets/icons/file-types/3d.svg
new file mode 100644
index 0000000..0fdb934
--- /dev/null
+++ b/ui/src/assets/icons/file-types/3d.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/README.md b/ui/src/assets/icons/file-types/README.md
new file mode 100644
index 0000000..9d252f9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/README.md
@@ -0,0 +1,26 @@
+# File Type Icons Sprite
+
+This directory keeps the source file-type SVG icons and the generated sprite used by the UI.
+
+## Runtime behavior
+
+- The UI resolves an icon id in `packages/ui/src/lib/fileTypeIcons.ts`.
+- Valid icon ids are loaded from `packages/ui/src/lib/fileTypeIconIds.ts`.
+- `packages/ui/src/components/icons/FileTypeIcon.tsx` renders the icon with ` ` from `sprite.svg`.
+- Vite handles `sprite.svg` as a normal asset URL automatically.
+
+The sprite generator rewrites internal SVG ids per icon (gradients, clip paths, filters) so ids do not collide after packing all icons into one file.
+
+## Build step
+
+- No special step is required for normal `dev`/`build`.
+- Regenerate the sprite only when icon source files in this folder change:
+
+```bash
+bun run icons:sprite
+```
+
+The command regenerates both `sprite.svg` and `packages/ui/src/lib/fileTypeIconIds.ts`.
+Both generated files are committed and consumed automatically by app builds.
+
+If you only run `bun run dev`, `bun run build`, `bun run lint`, or `bun run type-check`, no extra sprite step is needed unless the source icon files changed.
diff --git a/ui/src/assets/icons/file-types/abap.svg b/ui/src/assets/icons/file-types/abap.svg
new file mode 100644
index 0000000..0a9b083
--- /dev/null
+++ b/ui/src/assets/icons/file-types/abap.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/abc.svg b/ui/src/assets/icons/file-types/abc.svg
new file mode 100644
index 0000000..7c7cb53
--- /dev/null
+++ b/ui/src/assets/icons/file-types/abc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/actionscript.svg b/ui/src/assets/icons/file-types/actionscript.svg
new file mode 100644
index 0000000..31d91f2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/actionscript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ada.svg b/ui/src/assets/icons/file-types/ada.svg
new file mode 100644
index 0000000..613646f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ada.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/adobe-illustrator.svg b/ui/src/assets/icons/file-types/adobe-illustrator.svg
new file mode 100644
index 0000000..e0a334b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/adobe-illustrator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/adobe-illustrator_light.svg b/ui/src/assets/icons/file-types/adobe-illustrator_light.svg
new file mode 100644
index 0000000..326d231
--- /dev/null
+++ b/ui/src/assets/icons/file-types/adobe-illustrator_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/adobe-photoshop.svg b/ui/src/assets/icons/file-types/adobe-photoshop.svg
new file mode 100644
index 0000000..27033d9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/adobe-photoshop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/adobe-photoshop_light.svg b/ui/src/assets/icons/file-types/adobe-photoshop_light.svg
new file mode 100644
index 0000000..d2bfb4d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/adobe-photoshop_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/adobe-swc.svg b/ui/src/assets/icons/file-types/adobe-swc.svg
new file mode 100644
index 0000000..fda5c18
--- /dev/null
+++ b/ui/src/assets/icons/file-types/adobe-swc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/adonis.svg b/ui/src/assets/icons/file-types/adonis.svg
new file mode 100644
index 0000000..f854f01
--- /dev/null
+++ b/ui/src/assets/icons/file-types/adonis.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/advpl.svg b/ui/src/assets/icons/file-types/advpl.svg
new file mode 100644
index 0000000..54e493b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/advpl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/amplify.svg b/ui/src/assets/icons/file-types/amplify.svg
new file mode 100644
index 0000000..89f4212
--- /dev/null
+++ b/ui/src/assets/icons/file-types/amplify.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/android.svg b/ui/src/assets/icons/file-types/android.svg
new file mode 100644
index 0000000..c44608d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/android.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/angular.svg b/ui/src/assets/icons/file-types/angular.svg
new file mode 100644
index 0000000..a28075e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/angular.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/antlr.svg b/ui/src/assets/icons/file-types/antlr.svg
new file mode 100644
index 0000000..42f43bb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/antlr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/apiblueprint.svg b/ui/src/assets/icons/file-types/apiblueprint.svg
new file mode 100644
index 0000000..0846267
--- /dev/null
+++ b/ui/src/assets/icons/file-types/apiblueprint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/apollo.svg b/ui/src/assets/icons/file-types/apollo.svg
new file mode 100644
index 0000000..6de6aa2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/apollo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/applescript.svg b/ui/src/assets/icons/file-types/applescript.svg
new file mode 100644
index 0000000..d883e90
--- /dev/null
+++ b/ui/src/assets/icons/file-types/applescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/apps-script.svg b/ui/src/assets/icons/file-types/apps-script.svg
new file mode 100644
index 0000000..ed20f1f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/apps-script.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/appveyor.svg b/ui/src/assets/icons/file-types/appveyor.svg
new file mode 100644
index 0000000..0dd0a5c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/appveyor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/architecture.svg b/ui/src/assets/icons/file-types/architecture.svg
new file mode 100644
index 0000000..ee7de18
--- /dev/null
+++ b/ui/src/assets/icons/file-types/architecture.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/arduino.svg b/ui/src/assets/icons/file-types/arduino.svg
new file mode 100644
index 0000000..053dc12
--- /dev/null
+++ b/ui/src/assets/icons/file-types/arduino.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/asciidoc.svg b/ui/src/assets/icons/file-types/asciidoc.svg
new file mode 100644
index 0000000..82215c7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/asciidoc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/assembly.svg b/ui/src/assets/icons/file-types/assembly.svg
new file mode 100644
index 0000000..33a0566
--- /dev/null
+++ b/ui/src/assets/icons/file-types/assembly.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/astro-config.svg b/ui/src/assets/icons/file-types/astro-config.svg
new file mode 100644
index 0000000..1c12c5e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/astro-config.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/astro.svg b/ui/src/assets/icons/file-types/astro.svg
new file mode 100644
index 0000000..fa67fee
--- /dev/null
+++ b/ui/src/assets/icons/file-types/astro.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/astyle.svg b/ui/src/assets/icons/file-types/astyle.svg
new file mode 100644
index 0000000..6643432
--- /dev/null
+++ b/ui/src/assets/icons/file-types/astyle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/audio.svg b/ui/src/assets/icons/file-types/audio.svg
new file mode 100644
index 0000000..74f43c4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/audio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/aurelia.svg b/ui/src/assets/icons/file-types/aurelia.svg
new file mode 100644
index 0000000..f7b67f0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/aurelia.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/authors.svg b/ui/src/assets/icons/file-types/authors.svg
new file mode 100644
index 0000000..88618a7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/authors.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/auto.svg b/ui/src/assets/icons/file-types/auto.svg
new file mode 100644
index 0000000..41bd15d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/auto.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/auto_light.svg b/ui/src/assets/icons/file-types/auto_light.svg
new file mode 100644
index 0000000..5f2451b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/auto_light.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/autohotkey.svg b/ui/src/assets/icons/file-types/autohotkey.svg
new file mode 100644
index 0000000..4ecd7a3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/autohotkey.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/autoit.svg b/ui/src/assets/icons/file-types/autoit.svg
new file mode 100644
index 0000000..350519f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/autoit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/azure-pipelines.svg b/ui/src/assets/icons/file-types/azure-pipelines.svg
new file mode 100644
index 0000000..f460d20
--- /dev/null
+++ b/ui/src/assets/icons/file-types/azure-pipelines.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/azure.svg b/ui/src/assets/icons/file-types/azure.svg
new file mode 100644
index 0000000..2330f87
--- /dev/null
+++ b/ui/src/assets/icons/file-types/azure.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/babel.svg b/ui/src/assets/icons/file-types/babel.svg
new file mode 100644
index 0000000..244ae36
--- /dev/null
+++ b/ui/src/assets/icons/file-types/babel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ballerina.svg b/ui/src/assets/icons/file-types/ballerina.svg
new file mode 100644
index 0000000..3c1341d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ballerina.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bazel.svg b/ui/src/assets/icons/file-types/bazel.svg
new file mode 100644
index 0000000..b38a90c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bazel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bbx.svg b/ui/src/assets/icons/file-types/bbx.svg
new file mode 100644
index 0000000..002d260
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bbx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/beancount.svg b/ui/src/assets/icons/file-types/beancount.svg
new file mode 100644
index 0000000..905ff22
--- /dev/null
+++ b/ui/src/assets/icons/file-types/beancount.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bench-js.svg b/ui/src/assets/icons/file-types/bench-js.svg
new file mode 100644
index 0000000..c2ba0ca
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bench-js.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bench-jsx.svg b/ui/src/assets/icons/file-types/bench-jsx.svg
new file mode 100644
index 0000000..ed2b9d4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bench-jsx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bench-ts.svg b/ui/src/assets/icons/file-types/bench-ts.svg
new file mode 100644
index 0000000..f9c2af9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bench-ts.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bibliography.svg b/ui/src/assets/icons/file-types/bibliography.svg
new file mode 100644
index 0000000..ad6baa6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bibliography.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bibtex-style.svg b/ui/src/assets/icons/file-types/bibtex-style.svg
new file mode 100644
index 0000000..24d121d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bibtex-style.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bicep.svg b/ui/src/assets/icons/file-types/bicep.svg
new file mode 100644
index 0000000..dc959e7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bicep.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/biome.svg b/ui/src/assets/icons/file-types/biome.svg
new file mode 100644
index 0000000..2f255fc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/biome.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bitbucket.svg b/ui/src/assets/icons/file-types/bitbucket.svg
new file mode 100644
index 0000000..ba572f0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bitbucket.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bithound.svg b/ui/src/assets/icons/file-types/bithound.svg
new file mode 100644
index 0000000..1eea4de
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bithound.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/blender.svg b/ui/src/assets/icons/file-types/blender.svg
new file mode 100644
index 0000000..f55d6bc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/blender.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/blink.svg b/ui/src/assets/icons/file-types/blink.svg
new file mode 100644
index 0000000..4412288
--- /dev/null
+++ b/ui/src/assets/icons/file-types/blink.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/blink_light.svg b/ui/src/assets/icons/file-types/blink_light.svg
new file mode 100644
index 0000000..380d8c7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/blink_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/blitz.svg b/ui/src/assets/icons/file-types/blitz.svg
new file mode 100644
index 0000000..147ccc1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/blitz.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bower.svg b/ui/src/assets/icons/file-types/bower.svg
new file mode 100644
index 0000000..9ffb06a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bower.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/brainfuck.svg b/ui/src/assets/icons/file-types/brainfuck.svg
new file mode 100644
index 0000000..6a2422c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/brainfuck.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/browserlist.svg b/ui/src/assets/icons/file-types/browserlist.svg
new file mode 100644
index 0000000..d2e0d0a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/browserlist.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/browserlist_light.svg b/ui/src/assets/icons/file-types/browserlist_light.svg
new file mode 100644
index 0000000..fa34de6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/browserlist_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bruno.svg b/ui/src/assets/icons/file-types/bruno.svg
new file mode 100644
index 0000000..88bebea
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bruno.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/buck.svg b/ui/src/assets/icons/file-types/buck.svg
new file mode 100644
index 0000000..a5a31bc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/buck.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bucklescript.svg b/ui/src/assets/icons/file-types/bucklescript.svg
new file mode 100644
index 0000000..d67a784
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bucklescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/buildkite.svg b/ui/src/assets/icons/file-types/buildkite.svg
new file mode 100644
index 0000000..32a4995
--- /dev/null
+++ b/ui/src/assets/icons/file-types/buildkite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bun.svg b/ui/src/assets/icons/file-types/bun.svg
new file mode 100644
index 0000000..cc36204
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bun.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/bun_light.svg b/ui/src/assets/icons/file-types/bun_light.svg
new file mode 100644
index 0000000..d49bac7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/bun_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/c.svg b/ui/src/assets/icons/file-types/c.svg
new file mode 100644
index 0000000..5bb84b6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/c.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/c3.svg b/ui/src/assets/icons/file-types/c3.svg
new file mode 100644
index 0000000..ff30caa
--- /dev/null
+++ b/ui/src/assets/icons/file-types/c3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cabal.svg b/ui/src/assets/icons/file-types/cabal.svg
new file mode 100644
index 0000000..014335b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cabal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/caddy.svg b/ui/src/assets/icons/file-types/caddy.svg
new file mode 100644
index 0000000..997c119
--- /dev/null
+++ b/ui/src/assets/icons/file-types/caddy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cadence.svg b/ui/src/assets/icons/file-types/cadence.svg
new file mode 100644
index 0000000..25338ba
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cadence.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cairo.svg b/ui/src/assets/icons/file-types/cairo.svg
new file mode 100644
index 0000000..591b232
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cairo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cake.svg b/ui/src/assets/icons/file-types/cake.svg
new file mode 100644
index 0000000..ed6b09f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cake.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/capacitor.svg b/ui/src/assets/icons/file-types/capacitor.svg
new file mode 100644
index 0000000..2a48c58
--- /dev/null
+++ b/ui/src/assets/icons/file-types/capacitor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/capnp.svg b/ui/src/assets/icons/file-types/capnp.svg
new file mode 100644
index 0000000..c74aa9f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/capnp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cbx.svg b/ui/src/assets/icons/file-types/cbx.svg
new file mode 100644
index 0000000..716426a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cbx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cds.svg b/ui/src/assets/icons/file-types/cds.svg
new file mode 100644
index 0000000..3c7fed8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cds.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/certificate.svg b/ui/src/assets/icons/file-types/certificate.svg
new file mode 100644
index 0000000..64ddcf3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/certificate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/changelog.svg b/ui/src/assets/icons/file-types/changelog.svg
new file mode 100644
index 0000000..b4b1a07
--- /dev/null
+++ b/ui/src/assets/icons/file-types/changelog.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/chess.svg b/ui/src/assets/icons/file-types/chess.svg
new file mode 100644
index 0000000..85bede3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/chess.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/chess_light.svg b/ui/src/assets/icons/file-types/chess_light.svg
new file mode 100644
index 0000000..250fb8c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/chess_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/chrome.svg b/ui/src/assets/icons/file-types/chrome.svg
new file mode 100644
index 0000000..0208e27
--- /dev/null
+++ b/ui/src/assets/icons/file-types/chrome.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/circleci.svg b/ui/src/assets/icons/file-types/circleci.svg
new file mode 100644
index 0000000..464dace
--- /dev/null
+++ b/ui/src/assets/icons/file-types/circleci.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/circleci_light.svg b/ui/src/assets/icons/file-types/circleci_light.svg
new file mode 100644
index 0000000..cd45d35
--- /dev/null
+++ b/ui/src/assets/icons/file-types/circleci_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/citation.svg b/ui/src/assets/icons/file-types/citation.svg
new file mode 100644
index 0000000..eb7fcaa
--- /dev/null
+++ b/ui/src/assets/icons/file-types/citation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/clangd.svg b/ui/src/assets/icons/file-types/clangd.svg
new file mode 100644
index 0000000..f6742e9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/clangd.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/claude.svg b/ui/src/assets/icons/file-types/claude.svg
new file mode 100644
index 0000000..fa01860
--- /dev/null
+++ b/ui/src/assets/icons/file-types/claude.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cline.svg b/ui/src/assets/icons/file-types/cline.svg
new file mode 100644
index 0000000..c41f59d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/clojure.svg b/ui/src/assets/icons/file-types/clojure.svg
new file mode 100644
index 0000000..1b22aed
--- /dev/null
+++ b/ui/src/assets/icons/file-types/clojure.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cloudfoundry.svg b/ui/src/assets/icons/file-types/cloudfoundry.svg
new file mode 100644
index 0000000..3251ca4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cloudfoundry.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cmake.svg b/ui/src/assets/icons/file-types/cmake.svg
new file mode 100644
index 0000000..aa21796
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cmake.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/coala.svg b/ui/src/assets/icons/file-types/coala.svg
new file mode 100644
index 0000000..1e84b8f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/coala.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cobol.svg b/ui/src/assets/icons/file-types/cobol.svg
new file mode 100644
index 0000000..220b0ab
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cobol.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/coconut.svg b/ui/src/assets/icons/file-types/coconut.svg
new file mode 100644
index 0000000..98355a6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/coconut.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/code-climate.svg b/ui/src/assets/icons/file-types/code-climate.svg
new file mode 100644
index 0000000..97cbb4e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/code-climate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/code-climate_light.svg b/ui/src/assets/icons/file-types/code-climate_light.svg
new file mode 100644
index 0000000..dd18ba5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/code-climate_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/codecov.svg b/ui/src/assets/icons/file-types/codecov.svg
new file mode 100644
index 0000000..9a8d4eb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/codecov.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/codeowners.svg b/ui/src/assets/icons/file-types/codeowners.svg
new file mode 100644
index 0000000..553c60f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/codeowners.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/coderabbit-ai.svg b/ui/src/assets/icons/file-types/coderabbit-ai.svg
new file mode 100644
index 0000000..5d1b6c9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/coderabbit-ai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/coffee.svg b/ui/src/assets/icons/file-types/coffee.svg
new file mode 100644
index 0000000..f81b65c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/coffee.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/coldfusion.svg b/ui/src/assets/icons/file-types/coldfusion.svg
new file mode 100644
index 0000000..d018b66
--- /dev/null
+++ b/ui/src/assets/icons/file-types/coldfusion.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/coloredpetrinets.svg b/ui/src/assets/icons/file-types/coloredpetrinets.svg
new file mode 100644
index 0000000..bd61261
--- /dev/null
+++ b/ui/src/assets/icons/file-types/coloredpetrinets.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/command.svg b/ui/src/assets/icons/file-types/command.svg
new file mode 100644
index 0000000..b5a7913
--- /dev/null
+++ b/ui/src/assets/icons/file-types/command.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/commitizen.svg b/ui/src/assets/icons/file-types/commitizen.svg
new file mode 100644
index 0000000..2467d2c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/commitizen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/commitlint.svg b/ui/src/assets/icons/file-types/commitlint.svg
new file mode 100644
index 0000000..c42144a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/commitlint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/concourse.svg b/ui/src/assets/icons/file-types/concourse.svg
new file mode 100644
index 0000000..c34f23e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/concourse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/conduct.svg b/ui/src/assets/icons/file-types/conduct.svg
new file mode 100644
index 0000000..97eb6fc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/conduct.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/console.svg b/ui/src/assets/icons/file-types/console.svg
new file mode 100644
index 0000000..75f90b7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/console.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/contentlayer.svg b/ui/src/assets/icons/file-types/contentlayer.svg
new file mode 100644
index 0000000..441f690
--- /dev/null
+++ b/ui/src/assets/icons/file-types/contentlayer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/context.svg b/ui/src/assets/icons/file-types/context.svg
new file mode 100644
index 0000000..1b8200e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/context.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/contributing.svg b/ui/src/assets/icons/file-types/contributing.svg
new file mode 100644
index 0000000..13666a0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/contributing.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/controller.svg b/ui/src/assets/icons/file-types/controller.svg
new file mode 100644
index 0000000..9f99264
--- /dev/null
+++ b/ui/src/assets/icons/file-types/controller.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/copilot.svg b/ui/src/assets/icons/file-types/copilot.svg
new file mode 100644
index 0000000..24e89af
--- /dev/null
+++ b/ui/src/assets/icons/file-types/copilot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/copilot_light.svg b/ui/src/assets/icons/file-types/copilot_light.svg
new file mode 100644
index 0000000..9bc56ea
--- /dev/null
+++ b/ui/src/assets/icons/file-types/copilot_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cpp.svg b/ui/src/assets/icons/file-types/cpp.svg
new file mode 100644
index 0000000..16534ac
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cpp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/craco.svg b/ui/src/assets/icons/file-types/craco.svg
new file mode 100644
index 0000000..96ba458
--- /dev/null
+++ b/ui/src/assets/icons/file-types/craco.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/credits.svg b/ui/src/assets/icons/file-types/credits.svg
new file mode 100644
index 0000000..b67c55a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/credits.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/crystal.svg b/ui/src/assets/icons/file-types/crystal.svg
new file mode 100644
index 0000000..e3796bf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/crystal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/crystal_light.svg b/ui/src/assets/icons/file-types/crystal_light.svg
new file mode 100644
index 0000000..ca387f4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/crystal_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/csharp.svg b/ui/src/assets/icons/file-types/csharp.svg
new file mode 100644
index 0000000..02b1be3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/csharp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/css-map.svg b/ui/src/assets/icons/file-types/css-map.svg
new file mode 100644
index 0000000..55b74c0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/css-map.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/css.svg b/ui/src/assets/icons/file-types/css.svg
new file mode 100644
index 0000000..1acad1b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/css.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cucumber.svg b/ui/src/assets/icons/file-types/cucumber.svg
new file mode 100644
index 0000000..052fd29
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cucumber.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cuda.svg b/ui/src/assets/icons/file-types/cuda.svg
new file mode 100644
index 0000000..cc57a60
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cuda.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/cursor.svg b/ui/src/assets/icons/file-types/cursor.svg
new file mode 100644
index 0000000..b754147
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cursor.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/assets/icons/file-types/cursor_light.svg b/ui/src/assets/icons/file-types/cursor_light.svg
new file mode 100644
index 0000000..f65b646
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cursor_light.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/assets/icons/file-types/cypress.svg b/ui/src/assets/icons/file-types/cypress.svg
new file mode 100644
index 0000000..35274d3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/cypress.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/d.svg b/ui/src/assets/icons/file-types/d.svg
new file mode 100644
index 0000000..3207725
--- /dev/null
+++ b/ui/src/assets/icons/file-types/d.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/dart.svg b/ui/src/assets/icons/file-types/dart.svg
new file mode 100644
index 0000000..04b22d0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/dart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/dart_generated.svg b/ui/src/assets/icons/file-types/dart_generated.svg
new file mode 100644
index 0000000..8f64f5f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/dart_generated.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/database.svg b/ui/src/assets/icons/file-types/database.svg
new file mode 100644
index 0000000..b107234
--- /dev/null
+++ b/ui/src/assets/icons/file-types/database.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/deepsource.svg b/ui/src/assets/icons/file-types/deepsource.svg
new file mode 100644
index 0000000..d70fd46
--- /dev/null
+++ b/ui/src/assets/icons/file-types/deepsource.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/denizenscript.svg b/ui/src/assets/icons/file-types/denizenscript.svg
new file mode 100644
index 0000000..2debb9d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/denizenscript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/deno.svg b/ui/src/assets/icons/file-types/deno.svg
new file mode 100644
index 0000000..344a12e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/deno.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/deno_light.svg b/ui/src/assets/icons/file-types/deno_light.svg
new file mode 100644
index 0000000..ee82ea7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/deno_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/dependabot.svg b/ui/src/assets/icons/file-types/dependabot.svg
new file mode 100644
index 0000000..3b101a1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/dependabot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/dependencies-update.svg b/ui/src/assets/icons/file-types/dependencies-update.svg
new file mode 100644
index 0000000..b85ad9e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/dependencies-update.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/dhall.svg b/ui/src/assets/icons/file-types/dhall.svg
new file mode 100644
index 0000000..0be9411
--- /dev/null
+++ b/ui/src/assets/icons/file-types/dhall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/diff.svg b/ui/src/assets/icons/file-types/diff.svg
new file mode 100644
index 0000000..ea3068c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/diff.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/dinophp.svg b/ui/src/assets/icons/file-types/dinophp.svg
new file mode 100644
index 0000000..8e6ef29
--- /dev/null
+++ b/ui/src/assets/icons/file-types/dinophp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/disc.svg b/ui/src/assets/icons/file-types/disc.svg
new file mode 100644
index 0000000..b0d74dc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/disc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/django.svg b/ui/src/assets/icons/file-types/django.svg
new file mode 100644
index 0000000..64c9ee3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/django.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/dll.svg b/ui/src/assets/icons/file-types/dll.svg
new file mode 100644
index 0000000..0646cbb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/dll.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/docker.svg b/ui/src/assets/icons/file-types/docker.svg
new file mode 100644
index 0000000..7d6a1a5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/docker.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/doctex-installer.svg b/ui/src/assets/icons/file-types/doctex-installer.svg
new file mode 100644
index 0000000..5bdb443
--- /dev/null
+++ b/ui/src/assets/icons/file-types/doctex-installer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/document.svg b/ui/src/assets/icons/file-types/document.svg
new file mode 100644
index 0000000..a717956
--- /dev/null
+++ b/ui/src/assets/icons/file-types/document.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/dotjs.svg b/ui/src/assets/icons/file-types/dotjs.svg
new file mode 100644
index 0000000..5ac893c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/dotjs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/drawio.svg b/ui/src/assets/icons/file-types/drawio.svg
new file mode 100644
index 0000000..8ef1bcb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/drawio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/drizzle.svg b/ui/src/assets/icons/file-types/drizzle.svg
new file mode 100644
index 0000000..72f1b21
--- /dev/null
+++ b/ui/src/assets/icons/file-types/drizzle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/drone.svg b/ui/src/assets/icons/file-types/drone.svg
new file mode 100644
index 0000000..5e3082d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/drone.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/drone_light.svg b/ui/src/assets/icons/file-types/drone_light.svg
new file mode 100644
index 0000000..ce3ad25
--- /dev/null
+++ b/ui/src/assets/icons/file-types/drone_light.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/ui/src/assets/icons/file-types/duc.svg b/ui/src/assets/icons/file-types/duc.svg
new file mode 100644
index 0000000..1d85b34
--- /dev/null
+++ b/ui/src/assets/icons/file-types/duc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/dune.svg b/ui/src/assets/icons/file-types/dune.svg
new file mode 100644
index 0000000..1a35e70
--- /dev/null
+++ b/ui/src/assets/icons/file-types/dune.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/edge.svg b/ui/src/assets/icons/file-types/edge.svg
new file mode 100644
index 0000000..298b558
--- /dev/null
+++ b/ui/src/assets/icons/file-types/edge.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/editorconfig.svg b/ui/src/assets/icons/file-types/editorconfig.svg
new file mode 100644
index 0000000..ba52899
--- /dev/null
+++ b/ui/src/assets/icons/file-types/editorconfig.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ejs.svg b/ui/src/assets/icons/file-types/ejs.svg
new file mode 100644
index 0000000..6ead40e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ejs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/elixir.svg b/ui/src/assets/icons/file-types/elixir.svg
new file mode 100644
index 0000000..d40f90b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/elixir.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/elm.svg b/ui/src/assets/icons/file-types/elm.svg
new file mode 100644
index 0000000..c17b74d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/elm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/email.svg b/ui/src/assets/icons/file-types/email.svg
new file mode 100644
index 0000000..a603e14
--- /dev/null
+++ b/ui/src/assets/icons/file-types/email.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ember.svg b/ui/src/assets/icons/file-types/ember.svg
new file mode 100644
index 0000000..c16cef1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ember.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/epub.svg b/ui/src/assets/icons/file-types/epub.svg
new file mode 100644
index 0000000..98f11d4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/epub.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/erlang.svg b/ui/src/assets/icons/file-types/erlang.svg
new file mode 100644
index 0000000..41025d6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/erlang.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/esbuild.svg b/ui/src/assets/icons/file-types/esbuild.svg
new file mode 100644
index 0000000..e682d6b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/esbuild.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/eslint.svg b/ui/src/assets/icons/file-types/eslint.svg
new file mode 100644
index 0000000..54fe8cc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/eslint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/excalidraw.svg b/ui/src/assets/icons/file-types/excalidraw.svg
new file mode 100644
index 0000000..c1e1bca
--- /dev/null
+++ b/ui/src/assets/icons/file-types/excalidraw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/exe.svg b/ui/src/assets/icons/file-types/exe.svg
new file mode 100644
index 0000000..dde947d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/exe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/fastlane.svg b/ui/src/assets/icons/file-types/fastlane.svg
new file mode 100644
index 0000000..44d042f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/fastlane.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/favicon.svg b/ui/src/assets/icons/file-types/favicon.svg
new file mode 100644
index 0000000..21abf66
--- /dev/null
+++ b/ui/src/assets/icons/file-types/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/figma.svg b/ui/src/assets/icons/file-types/figma.svg
new file mode 100644
index 0000000..db4522b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/figma.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/firebase.svg b/ui/src/assets/icons/file-types/firebase.svg
new file mode 100644
index 0000000..bb3b63c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/firebase.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/flash.svg b/ui/src/assets/icons/file-types/flash.svg
new file mode 100644
index 0000000..abd6e01
--- /dev/null
+++ b/ui/src/assets/icons/file-types/flash.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/flow.svg b/ui/src/assets/icons/file-types/flow.svg
new file mode 100644
index 0000000..0591981
--- /dev/null
+++ b/ui/src/assets/icons/file-types/flow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-admin-open.svg b/ui/src/assets/icons/file-types/folder-admin-open.svg
new file mode 100644
index 0000000..5e77464
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-admin-open.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-admin.svg b/ui/src/assets/icons/file-types/folder-admin.svg
new file mode 100644
index 0000000..f8d1ea1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-admin.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-android-open.svg b/ui/src/assets/icons/file-types/folder-android-open.svg
new file mode 100644
index 0000000..cdd8376
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-android-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-android.svg b/ui/src/assets/icons/file-types/folder-android.svg
new file mode 100644
index 0000000..7ee8a46
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-android.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-angular-open.svg b/ui/src/assets/icons/file-types/folder-angular-open.svg
new file mode 100644
index 0000000..60c604e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-angular-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-angular.svg b/ui/src/assets/icons/file-types/folder-angular.svg
new file mode 100644
index 0000000..3d8c87d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-angular.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-animation-open.svg b/ui/src/assets/icons/file-types/folder-animation-open.svg
new file mode 100644
index 0000000..637a3af
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-animation-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-animation.svg b/ui/src/assets/icons/file-types/folder-animation.svg
new file mode 100644
index 0000000..6b5bb69
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-animation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-ansible-open.svg b/ui/src/assets/icons/file-types/folder-ansible-open.svg
new file mode 100644
index 0000000..96df458
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ansible-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-ansible.svg b/ui/src/assets/icons/file-types/folder-ansible.svg
new file mode 100644
index 0000000..f430315
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ansible.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-api-open.svg b/ui/src/assets/icons/file-types/folder-api-open.svg
new file mode 100644
index 0000000..ac3edb9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-api-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-api.svg b/ui/src/assets/icons/file-types/folder-api.svg
new file mode 100644
index 0000000..bf1d64c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-api.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-apollo-open.svg b/ui/src/assets/icons/file-types/folder-apollo-open.svg
new file mode 100644
index 0000000..f0febaf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-apollo-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-apollo.svg b/ui/src/assets/icons/file-types/folder-apollo.svg
new file mode 100644
index 0000000..7eb6107
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-apollo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-app-open.svg b/ui/src/assets/icons/file-types/folder-app-open.svg
new file mode 100644
index 0000000..c9da6a7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-app-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-app.svg b/ui/src/assets/icons/file-types/folder-app.svg
new file mode 100644
index 0000000..d0e37f1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-app.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-archive-open.svg b/ui/src/assets/icons/file-types/folder-archive-open.svg
new file mode 100644
index 0000000..6af2a9f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-archive-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-archive.svg b/ui/src/assets/icons/file-types/folder-archive.svg
new file mode 100644
index 0000000..b018654
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-archive.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-astro-open.svg b/ui/src/assets/icons/file-types/folder-astro-open.svg
new file mode 100644
index 0000000..282a3ce
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-astro-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-astro.svg b/ui/src/assets/icons/file-types/folder-astro.svg
new file mode 100644
index 0000000..b324019
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-astro.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-atom-open.svg b/ui/src/assets/icons/file-types/folder-atom-open.svg
new file mode 100644
index 0000000..5558d18
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-atom-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-atom.svg b/ui/src/assets/icons/file-types/folder-atom.svg
new file mode 100644
index 0000000..c272f6e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-atom.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-attachment-open.svg b/ui/src/assets/icons/file-types/folder-attachment-open.svg
new file mode 100644
index 0000000..7a9af66
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-attachment-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-attachment.svg b/ui/src/assets/icons/file-types/folder-attachment.svg
new file mode 100644
index 0000000..3b9992e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-attachment.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-audio-open.svg b/ui/src/assets/icons/file-types/folder-audio-open.svg
new file mode 100644
index 0000000..6d9b238
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-audio-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-audio.svg b/ui/src/assets/icons/file-types/folder-audio.svg
new file mode 100644
index 0000000..e3d0db3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-audio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-aurelia-open.svg b/ui/src/assets/icons/file-types/folder-aurelia-open.svg
new file mode 100644
index 0000000..bacae24
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-aurelia-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-aurelia.svg b/ui/src/assets/icons/file-types/folder-aurelia.svg
new file mode 100644
index 0000000..61ee59e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-aurelia.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-aws-open.svg b/ui/src/assets/icons/file-types/folder-aws-open.svg
new file mode 100644
index 0000000..9e530d4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-aws-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-aws.svg b/ui/src/assets/icons/file-types/folder-aws.svg
new file mode 100644
index 0000000..769755d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-aws.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-azure-pipelines-open.svg b/ui/src/assets/icons/file-types/folder-azure-pipelines-open.svg
new file mode 100644
index 0000000..9253cd5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-azure-pipelines-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-azure-pipelines.svg b/ui/src/assets/icons/file-types/folder-azure-pipelines.svg
new file mode 100644
index 0000000..a0fef25
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-azure-pipelines.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-backup-open.svg b/ui/src/assets/icons/file-types/folder-backup-open.svg
new file mode 100644
index 0000000..c2914ee
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-backup-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-backup.svg b/ui/src/assets/icons/file-types/folder-backup.svg
new file mode 100644
index 0000000..aa9a6c9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-backup.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-base-open.svg b/ui/src/assets/icons/file-types/folder-base-open.svg
new file mode 100644
index 0000000..e84bc36
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-base-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-base.svg b/ui/src/assets/icons/file-types/folder-base.svg
new file mode 100644
index 0000000..1944100
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-base.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-batch-open.svg b/ui/src/assets/icons/file-types/folder-batch-open.svg
new file mode 100644
index 0000000..1db45e1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-batch-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-batch.svg b/ui/src/assets/icons/file-types/folder-batch.svg
new file mode 100644
index 0000000..c44a66b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-batch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-benchmark-open.svg b/ui/src/assets/icons/file-types/folder-benchmark-open.svg
new file mode 100644
index 0000000..fa7b3ea
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-benchmark-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-benchmark.svg b/ui/src/assets/icons/file-types/folder-benchmark.svg
new file mode 100644
index 0000000..8291d68
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-benchmark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-bibliography-open.svg b/ui/src/assets/icons/file-types/folder-bibliography-open.svg
new file mode 100644
index 0000000..81b6cde
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-bibliography-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-bibliography.svg b/ui/src/assets/icons/file-types/folder-bibliography.svg
new file mode 100644
index 0000000..aa1e92a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-bibliography.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-bicep-open.svg b/ui/src/assets/icons/file-types/folder-bicep-open.svg
new file mode 100644
index 0000000..72519ce
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-bicep-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-bicep.svg b/ui/src/assets/icons/file-types/folder-bicep.svg
new file mode 100644
index 0000000..b336ff5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-bicep.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-blender-open.svg b/ui/src/assets/icons/file-types/folder-blender-open.svg
new file mode 100644
index 0000000..1c80d73
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-blender-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-blender.svg b/ui/src/assets/icons/file-types/folder-blender.svg
new file mode 100644
index 0000000..6f56dce
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-blender.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-bloc-open.svg b/ui/src/assets/icons/file-types/folder-bloc-open.svg
new file mode 100644
index 0000000..8833e5f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-bloc-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-bloc.svg b/ui/src/assets/icons/file-types/folder-bloc.svg
new file mode 100644
index 0000000..cf08363
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-bloc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-bower-open.svg b/ui/src/assets/icons/file-types/folder-bower-open.svg
new file mode 100644
index 0000000..659f87c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-bower-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-bower.svg b/ui/src/assets/icons/file-types/folder-bower.svg
new file mode 100644
index 0000000..6bfd654
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-bower.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-buildkite-open.svg b/ui/src/assets/icons/file-types/folder-buildkite-open.svg
new file mode 100644
index 0000000..872db64
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-buildkite-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-buildkite.svg b/ui/src/assets/icons/file-types/folder-buildkite.svg
new file mode 100644
index 0000000..9512b40
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-buildkite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cart-open.svg b/ui/src/assets/icons/file-types/folder-cart-open.svg
new file mode 100644
index 0000000..4471a77
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cart-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cart.svg b/ui/src/assets/icons/file-types/folder-cart.svg
new file mode 100644
index 0000000..d19a627
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-changesets-open.svg b/ui/src/assets/icons/file-types/folder-changesets-open.svg
new file mode 100644
index 0000000..c389233
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-changesets-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-changesets.svg b/ui/src/assets/icons/file-types/folder-changesets.svg
new file mode 100644
index 0000000..fc071f4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-changesets.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-ci-open.svg b/ui/src/assets/icons/file-types/folder-ci-open.svg
new file mode 100644
index 0000000..57ac1ba
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ci-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-ci.svg b/ui/src/assets/icons/file-types/folder-ci.svg
new file mode 100644
index 0000000..4fdc2ed
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ci.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-circleci-open.svg b/ui/src/assets/icons/file-types/folder-circleci-open.svg
new file mode 100644
index 0000000..9e323ff
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-circleci-open.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-circleci.svg b/ui/src/assets/icons/file-types/folder-circleci.svg
new file mode 100644
index 0000000..ef32518
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-circleci.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-class-open.svg b/ui/src/assets/icons/file-types/folder-class-open.svg
new file mode 100644
index 0000000..9c5b101
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-class-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-class.svg b/ui/src/assets/icons/file-types/folder-class.svg
new file mode 100644
index 0000000..8225cf1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-class.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-claude-open.svg b/ui/src/assets/icons/file-types/folder-claude-open.svg
new file mode 100644
index 0000000..1a52afd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-claude-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-claude.svg b/ui/src/assets/icons/file-types/folder-claude.svg
new file mode 100644
index 0000000..0c85a13
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-claude.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-client-open.svg b/ui/src/assets/icons/file-types/folder-client-open.svg
new file mode 100644
index 0000000..ceec8f1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-client-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-client.svg b/ui/src/assets/icons/file-types/folder-client.svg
new file mode 100644
index 0000000..fbfaee7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-client.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cline-open.svg b/ui/src/assets/icons/file-types/folder-cline-open.svg
new file mode 100644
index 0000000..67ef7a2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cline-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cline.svg b/ui/src/assets/icons/file-types/folder-cline.svg
new file mode 100644
index 0000000..8fec96d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cloud-functions-open.svg b/ui/src/assets/icons/file-types/folder-cloud-functions-open.svg
new file mode 100644
index 0000000..b3ce0e4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cloud-functions-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cloud-functions.svg b/ui/src/assets/icons/file-types/folder-cloud-functions.svg
new file mode 100644
index 0000000..8dac84a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cloud-functions.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cloudflare-open.svg b/ui/src/assets/icons/file-types/folder-cloudflare-open.svg
new file mode 100644
index 0000000..d7022ab
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cloudflare-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cloudflare.svg b/ui/src/assets/icons/file-types/folder-cloudflare.svg
new file mode 100644
index 0000000..0cc444e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cloudflare.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cluster-open.svg b/ui/src/assets/icons/file-types/folder-cluster-open.svg
new file mode 100644
index 0000000..3688433
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cluster-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cluster.svg b/ui/src/assets/icons/file-types/folder-cluster.svg
new file mode 100644
index 0000000..77f5b8a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cluster.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cobol-open.svg b/ui/src/assets/icons/file-types/folder-cobol-open.svg
new file mode 100644
index 0000000..0f5e315
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cobol-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cobol.svg b/ui/src/assets/icons/file-types/folder-cobol.svg
new file mode 100644
index 0000000..ea0f54d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cobol.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-command-open.svg b/ui/src/assets/icons/file-types/folder-command-open.svg
new file mode 100644
index 0000000..ca9d4df
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-command-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-command.svg b/ui/src/assets/icons/file-types/folder-command.svg
new file mode 100644
index 0000000..4015207
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-command.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-components-open.svg b/ui/src/assets/icons/file-types/folder-components-open.svg
new file mode 100644
index 0000000..2f55b72
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-components-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-components.svg b/ui/src/assets/icons/file-types/folder-components.svg
new file mode 100644
index 0000000..983833e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-components.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-config-open.svg b/ui/src/assets/icons/file-types/folder-config-open.svg
new file mode 100644
index 0000000..3b4ec5a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-config-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-config.svg b/ui/src/assets/icons/file-types/folder-config.svg
new file mode 100644
index 0000000..8519910
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-config.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-connection-open.svg b/ui/src/assets/icons/file-types/folder-connection-open.svg
new file mode 100644
index 0000000..4d14f09
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-connection-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-connection.svg b/ui/src/assets/icons/file-types/folder-connection.svg
new file mode 100644
index 0000000..f46d526
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-connection.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-console-open.svg b/ui/src/assets/icons/file-types/folder-console-open.svg
new file mode 100644
index 0000000..99384a8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-console-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-console.svg b/ui/src/assets/icons/file-types/folder-console.svg
new file mode 100644
index 0000000..301b10d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-console.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-constant-open.svg b/ui/src/assets/icons/file-types/folder-constant-open.svg
new file mode 100644
index 0000000..9e8791d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-constant-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-constant.svg b/ui/src/assets/icons/file-types/folder-constant.svg
new file mode 100644
index 0000000..99a2291
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-constant.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-container-open.svg b/ui/src/assets/icons/file-types/folder-container-open.svg
new file mode 100644
index 0000000..9db8334
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-container-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-container.svg b/ui/src/assets/icons/file-types/folder-container.svg
new file mode 100644
index 0000000..3ea03c1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-container.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-content-open.svg b/ui/src/assets/icons/file-types/folder-content-open.svg
new file mode 100644
index 0000000..a924b27
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-content-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-content.svg b/ui/src/assets/icons/file-types/folder-content.svg
new file mode 100644
index 0000000..23f57d2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-content.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-context-open.svg b/ui/src/assets/icons/file-types/folder-context-open.svg
new file mode 100644
index 0000000..a631e02
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-context-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-context.svg b/ui/src/assets/icons/file-types/folder-context.svg
new file mode 100644
index 0000000..bee74c1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-context.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-contract-open.svg b/ui/src/assets/icons/file-types/folder-contract-open.svg
new file mode 100644
index 0000000..6878c76
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-contract-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-contract.svg b/ui/src/assets/icons/file-types/folder-contract.svg
new file mode 100644
index 0000000..2ea0abb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-contract.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-controller-open.svg b/ui/src/assets/icons/file-types/folder-controller-open.svg
new file mode 100644
index 0000000..a732ed1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-controller-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-controller.svg b/ui/src/assets/icons/file-types/folder-controller.svg
new file mode 100644
index 0000000..f98cd6f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-controller.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-core-open.svg b/ui/src/assets/icons/file-types/folder-core-open.svg
new file mode 100644
index 0000000..34e7a82
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-core-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-core.svg b/ui/src/assets/icons/file-types/folder-core.svg
new file mode 100644
index 0000000..f7cfae6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-core.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-coverage-open.svg b/ui/src/assets/icons/file-types/folder-coverage-open.svg
new file mode 100644
index 0000000..5d47b2f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-coverage-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-coverage.svg b/ui/src/assets/icons/file-types/folder-coverage.svg
new file mode 100644
index 0000000..7a75f71
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-coverage.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-css-open.svg b/ui/src/assets/icons/file-types/folder-css-open.svg
new file mode 100644
index 0000000..ef79791
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-css-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-css.svg b/ui/src/assets/icons/file-types/folder-css.svg
new file mode 100644
index 0000000..4ff433e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-css.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cursor-open.svg b/ui/src/assets/icons/file-types/folder-cursor-open.svg
new file mode 100644
index 0000000..b6e1068
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cursor-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cursor-open_light.svg b/ui/src/assets/icons/file-types/folder-cursor-open_light.svg
new file mode 100644
index 0000000..c960112
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cursor-open_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cursor.svg b/ui/src/assets/icons/file-types/folder-cursor.svg
new file mode 100644
index 0000000..4672608
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cursor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cursor_light.svg b/ui/src/assets/icons/file-types/folder-cursor_light.svg
new file mode 100644
index 0000000..391be56
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cursor_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-custom-open.svg b/ui/src/assets/icons/file-types/folder-custom-open.svg
new file mode 100644
index 0000000..fe747d2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-custom-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-custom.svg b/ui/src/assets/icons/file-types/folder-custom.svg
new file mode 100644
index 0000000..02ac611
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-custom.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cypress-open.svg b/ui/src/assets/icons/file-types/folder-cypress-open.svg
new file mode 100644
index 0000000..2a18521
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cypress-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-cypress.svg b/ui/src/assets/icons/file-types/folder-cypress.svg
new file mode 100644
index 0000000..39460e2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-cypress.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-dart-open.svg b/ui/src/assets/icons/file-types/folder-dart-open.svg
new file mode 100644
index 0000000..8eadca0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-dart-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-dart.svg b/ui/src/assets/icons/file-types/folder-dart.svg
new file mode 100644
index 0000000..0de1518
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-dart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-database-open.svg b/ui/src/assets/icons/file-types/folder-database-open.svg
new file mode 100644
index 0000000..5bde146
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-database-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-database.svg b/ui/src/assets/icons/file-types/folder-database.svg
new file mode 100644
index 0000000..b256e64
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-database.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-debug-open.svg b/ui/src/assets/icons/file-types/folder-debug-open.svg
new file mode 100644
index 0000000..a0c16a7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-debug-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-debug.svg b/ui/src/assets/icons/file-types/folder-debug.svg
new file mode 100644
index 0000000..1099873
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-debug.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-decorators-open.svg b/ui/src/assets/icons/file-types/folder-decorators-open.svg
new file mode 100644
index 0000000..ff42dde
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-decorators-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-decorators.svg b/ui/src/assets/icons/file-types/folder-decorators.svg
new file mode 100644
index 0000000..fcc746d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-decorators.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-delta-open.svg b/ui/src/assets/icons/file-types/folder-delta-open.svg
new file mode 100644
index 0000000..c2b5663
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-delta-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-delta.svg b/ui/src/assets/icons/file-types/folder-delta.svg
new file mode 100644
index 0000000..cdda479
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-delta.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-desktop-open.svg b/ui/src/assets/icons/file-types/folder-desktop-open.svg
new file mode 100644
index 0000000..880ca76
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-desktop-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-desktop.svg b/ui/src/assets/icons/file-types/folder-desktop.svg
new file mode 100644
index 0000000..5a20b49
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-desktop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-directive-open.svg b/ui/src/assets/icons/file-types/folder-directive-open.svg
new file mode 100644
index 0000000..71946e5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-directive-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-directive.svg b/ui/src/assets/icons/file-types/folder-directive.svg
new file mode 100644
index 0000000..4197c68
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-directive.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-dist-open.svg b/ui/src/assets/icons/file-types/folder-dist-open.svg
new file mode 100644
index 0000000..553cef1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-dist-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-dist.svg b/ui/src/assets/icons/file-types/folder-dist.svg
new file mode 100644
index 0000000..995580f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-dist.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-docker-open.svg b/ui/src/assets/icons/file-types/folder-docker-open.svg
new file mode 100644
index 0000000..a76e97b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-docker-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-docker.svg b/ui/src/assets/icons/file-types/folder-docker.svg
new file mode 100644
index 0000000..c5b0949
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-docker.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-docs-open.svg b/ui/src/assets/icons/file-types/folder-docs-open.svg
new file mode 100644
index 0000000..3577767
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-docs-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-docs.svg b/ui/src/assets/icons/file-types/folder-docs.svg
new file mode 100644
index 0000000..246a05d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-docs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-download-open.svg b/ui/src/assets/icons/file-types/folder-download-open.svg
new file mode 100644
index 0000000..ddb9c24
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-download-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-download.svg b/ui/src/assets/icons/file-types/folder-download.svg
new file mode 100644
index 0000000..34105b9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-drizzle-open.svg b/ui/src/assets/icons/file-types/folder-drizzle-open.svg
new file mode 100644
index 0000000..5f0cd59
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-drizzle-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-drizzle.svg b/ui/src/assets/icons/file-types/folder-drizzle.svg
new file mode 100644
index 0000000..d01a186
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-drizzle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-dump-open.svg b/ui/src/assets/icons/file-types/folder-dump-open.svg
new file mode 100644
index 0000000..b4de7f8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-dump-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-dump.svg b/ui/src/assets/icons/file-types/folder-dump.svg
new file mode 100644
index 0000000..8178fcc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-dump.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-element-open.svg b/ui/src/assets/icons/file-types/folder-element-open.svg
new file mode 100644
index 0000000..32dc7cd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-element-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-element.svg b/ui/src/assets/icons/file-types/folder-element.svg
new file mode 100644
index 0000000..d67a85a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-element.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-enum-open.svg b/ui/src/assets/icons/file-types/folder-enum-open.svg
new file mode 100644
index 0000000..92782b1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-enum-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-enum.svg b/ui/src/assets/icons/file-types/folder-enum.svg
new file mode 100644
index 0000000..fa852ef
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-enum.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-environment-open.svg b/ui/src/assets/icons/file-types/folder-environment-open.svg
new file mode 100644
index 0000000..3b56abb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-environment-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-environment.svg b/ui/src/assets/icons/file-types/folder-environment.svg
new file mode 100644
index 0000000..9cc1f2e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-environment.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-error-open.svg b/ui/src/assets/icons/file-types/folder-error-open.svg
new file mode 100644
index 0000000..81f0ffc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-error-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-error.svg b/ui/src/assets/icons/file-types/folder-error.svg
new file mode 100644
index 0000000..3bd1d85
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-error.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-event-open.svg b/ui/src/assets/icons/file-types/folder-event-open.svg
new file mode 100644
index 0000000..28c018d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-event-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-event.svg b/ui/src/assets/icons/file-types/folder-event.svg
new file mode 100644
index 0000000..f54dea6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-event.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-examples-open.svg b/ui/src/assets/icons/file-types/folder-examples-open.svg
new file mode 100644
index 0000000..78c77a9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-examples-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-examples.svg b/ui/src/assets/icons/file-types/folder-examples.svg
new file mode 100644
index 0000000..fba8885
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-examples.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-expo-open.svg b/ui/src/assets/icons/file-types/folder-expo-open.svg
new file mode 100644
index 0000000..614435a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-expo-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-expo.svg b/ui/src/assets/icons/file-types/folder-expo.svg
new file mode 100644
index 0000000..820a998
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-expo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-export-open.svg b/ui/src/assets/icons/file-types/folder-export-open.svg
new file mode 100644
index 0000000..f03eb1b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-export-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-export.svg b/ui/src/assets/icons/file-types/folder-export.svg
new file mode 100644
index 0000000..1b3e3ab
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-export.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-fastlane-open.svg b/ui/src/assets/icons/file-types/folder-fastlane-open.svg
new file mode 100644
index 0000000..5efb2ac
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-fastlane-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-fastlane.svg b/ui/src/assets/icons/file-types/folder-fastlane.svg
new file mode 100644
index 0000000..eb90566
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-fastlane.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-favicon-open.svg b/ui/src/assets/icons/file-types/folder-favicon-open.svg
new file mode 100644
index 0000000..b716525
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-favicon-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-favicon.svg b/ui/src/assets/icons/file-types/folder-favicon.svg
new file mode 100644
index 0000000..6ef90d9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-firebase-open.svg b/ui/src/assets/icons/file-types/folder-firebase-open.svg
new file mode 100644
index 0000000..7149b48
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-firebase-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-firebase.svg b/ui/src/assets/icons/file-types/folder-firebase.svg
new file mode 100644
index 0000000..9eeac86
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-firebase.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-firestore-open.svg b/ui/src/assets/icons/file-types/folder-firestore-open.svg
new file mode 100644
index 0000000..a3e6eda
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-firestore-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-firestore.svg b/ui/src/assets/icons/file-types/folder-firestore.svg
new file mode 100644
index 0000000..cb1249a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-firestore.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-flow-open.svg b/ui/src/assets/icons/file-types/folder-flow-open.svg
new file mode 100644
index 0000000..a72dd76
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-flow-open.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-flow.svg b/ui/src/assets/icons/file-types/folder-flow.svg
new file mode 100644
index 0000000..0155189
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-flow.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-flutter-open.svg b/ui/src/assets/icons/file-types/folder-flutter-open.svg
new file mode 100644
index 0000000..b95a8ce
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-flutter-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-flutter.svg b/ui/src/assets/icons/file-types/folder-flutter.svg
new file mode 100644
index 0000000..e5ffced
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-flutter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-font-open.svg b/ui/src/assets/icons/file-types/folder-font-open.svg
new file mode 100644
index 0000000..1a91f0b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-font-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-font.svg b/ui/src/assets/icons/file-types/folder-font.svg
new file mode 100644
index 0000000..0115b73
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-font.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-forgejo-open.svg b/ui/src/assets/icons/file-types/folder-forgejo-open.svg
new file mode 100644
index 0000000..a976222
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-forgejo-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-forgejo.svg b/ui/src/assets/icons/file-types/folder-forgejo.svg
new file mode 100644
index 0000000..0eaccff
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-forgejo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-functions-open.svg b/ui/src/assets/icons/file-types/folder-functions-open.svg
new file mode 100644
index 0000000..00d6dc4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-functions-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-functions.svg b/ui/src/assets/icons/file-types/folder-functions.svg
new file mode 100644
index 0000000..01a9385
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-functions.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gamemaker-open.svg b/ui/src/assets/icons/file-types/folder-gamemaker-open.svg
new file mode 100644
index 0000000..caf9a82
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gamemaker-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gamemaker.svg b/ui/src/assets/icons/file-types/folder-gamemaker.svg
new file mode 100644
index 0000000..625feb3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gamemaker.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-generator-open.svg b/ui/src/assets/icons/file-types/folder-generator-open.svg
new file mode 100644
index 0000000..43b5047
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-generator-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-generator.svg b/ui/src/assets/icons/file-types/folder-generator.svg
new file mode 100644
index 0000000..5446582
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-generator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gh-workflows-open.svg b/ui/src/assets/icons/file-types/folder-gh-workflows-open.svg
new file mode 100644
index 0000000..3ae400e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gh-workflows-open.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-gh-workflows.svg b/ui/src/assets/icons/file-types/folder-gh-workflows.svg
new file mode 100644
index 0000000..3a868cc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gh-workflows.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-git-open.svg b/ui/src/assets/icons/file-types/folder-git-open.svg
new file mode 100644
index 0000000..90be1c1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-git-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-git.svg b/ui/src/assets/icons/file-types/folder-git.svg
new file mode 100644
index 0000000..2ca4db5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-git.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gitea-open.svg b/ui/src/assets/icons/file-types/folder-gitea-open.svg
new file mode 100644
index 0000000..239800c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gitea-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gitea.svg b/ui/src/assets/icons/file-types/folder-gitea.svg
new file mode 100644
index 0000000..ac041b3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gitea.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-github-open.svg b/ui/src/assets/icons/file-types/folder-github-open.svg
new file mode 100644
index 0000000..84e5bee
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-github-open.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-github.svg b/ui/src/assets/icons/file-types/folder-github.svg
new file mode 100644
index 0000000..374bcae
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-github.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-gitlab-open.svg b/ui/src/assets/icons/file-types/folder-gitlab-open.svg
new file mode 100644
index 0000000..fc4deb2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gitlab-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gitlab.svg b/ui/src/assets/icons/file-types/folder-gitlab.svg
new file mode 100644
index 0000000..55db99e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gitlab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-global-open.svg b/ui/src/assets/icons/file-types/folder-global-open.svg
new file mode 100644
index 0000000..13e72e0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-global-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-global.svg b/ui/src/assets/icons/file-types/folder-global.svg
new file mode 100644
index 0000000..8ada6a6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-global.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-godot-open.svg b/ui/src/assets/icons/file-types/folder-godot-open.svg
new file mode 100644
index 0000000..fd78550
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-godot-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-godot.svg b/ui/src/assets/icons/file-types/folder-godot.svg
new file mode 100644
index 0000000..dc4b5d1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-godot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gradle-open.svg b/ui/src/assets/icons/file-types/folder-gradle-open.svg
new file mode 100644
index 0000000..51725e7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gradle-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gradle.svg b/ui/src/assets/icons/file-types/folder-gradle.svg
new file mode 100644
index 0000000..93e843d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gradle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-graphql-open.svg b/ui/src/assets/icons/file-types/folder-graphql-open.svg
new file mode 100644
index 0000000..ac23650
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-graphql-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-graphql.svg b/ui/src/assets/icons/file-types/folder-graphql.svg
new file mode 100644
index 0000000..1d7b1cc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-graphql.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-guard-open.svg b/ui/src/assets/icons/file-types/folder-guard-open.svg
new file mode 100644
index 0000000..f7031e2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-guard-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-guard.svg b/ui/src/assets/icons/file-types/folder-guard.svg
new file mode 100644
index 0000000..b4269ed
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-guard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gulp-open.svg b/ui/src/assets/icons/file-types/folder-gulp-open.svg
new file mode 100644
index 0000000..556e739
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gulp-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-gulp.svg b/ui/src/assets/icons/file-types/folder-gulp.svg
new file mode 100644
index 0000000..3395231
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-gulp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-helm-open.svg b/ui/src/assets/icons/file-types/folder-helm-open.svg
new file mode 100644
index 0000000..6bbf0cc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-helm-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-helm.svg b/ui/src/assets/icons/file-types/folder-helm.svg
new file mode 100644
index 0000000..7b7d7a7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-helm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-helper-open.svg b/ui/src/assets/icons/file-types/folder-helper-open.svg
new file mode 100644
index 0000000..6fca391
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-helper-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-helper.svg b/ui/src/assets/icons/file-types/folder-helper.svg
new file mode 100644
index 0000000..27a20d4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-helper.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-home-open.svg b/ui/src/assets/icons/file-types/folder-home-open.svg
new file mode 100644
index 0000000..8b0f0ca
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-home-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-home.svg b/ui/src/assets/icons/file-types/folder-home.svg
new file mode 100644
index 0000000..a4deeef
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-home.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-hook-open.svg b/ui/src/assets/icons/file-types/folder-hook-open.svg
new file mode 100644
index 0000000..17d6231
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-hook-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-hook.svg b/ui/src/assets/icons/file-types/folder-hook.svg
new file mode 100644
index 0000000..2105709
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-hook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-husky-open.svg b/ui/src/assets/icons/file-types/folder-husky-open.svg
new file mode 100644
index 0000000..88c19e8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-husky-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-husky.svg b/ui/src/assets/icons/file-types/folder-husky.svg
new file mode 100644
index 0000000..1bbdc4c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-husky.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-i18n-open.svg b/ui/src/assets/icons/file-types/folder-i18n-open.svg
new file mode 100644
index 0000000..bc1a53c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-i18n-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-i18n.svg b/ui/src/assets/icons/file-types/folder-i18n.svg
new file mode 100644
index 0000000..6ef0283
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-i18n.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-images-open.svg b/ui/src/assets/icons/file-types/folder-images-open.svg
new file mode 100644
index 0000000..44a673b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-images-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-images.svg b/ui/src/assets/icons/file-types/folder-images.svg
new file mode 100644
index 0000000..5b63a6c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-images.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-import-open.svg b/ui/src/assets/icons/file-types/folder-import-open.svg
new file mode 100644
index 0000000..a58a7e6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-import-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-import.svg b/ui/src/assets/icons/file-types/folder-import.svg
new file mode 100644
index 0000000..0c0f42e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-import.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-include-open.svg b/ui/src/assets/icons/file-types/folder-include-open.svg
new file mode 100644
index 0000000..fc2c011
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-include-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-include.svg b/ui/src/assets/icons/file-types/folder-include.svg
new file mode 100644
index 0000000..117b91a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-include.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-intellij-open.svg b/ui/src/assets/icons/file-types/folder-intellij-open.svg
new file mode 100644
index 0000000..5839a2b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-intellij-open.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-intellij-open_light.svg b/ui/src/assets/icons/file-types/folder-intellij-open_light.svg
new file mode 100644
index 0000000..ccb6046
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-intellij-open_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-intellij.svg b/ui/src/assets/icons/file-types/folder-intellij.svg
new file mode 100644
index 0000000..c655f37
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-intellij.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-intellij_light.svg b/ui/src/assets/icons/file-types/folder-intellij_light.svg
new file mode 100644
index 0000000..97bc8c7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-intellij_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-interceptor-open.svg b/ui/src/assets/icons/file-types/folder-interceptor-open.svg
new file mode 100644
index 0000000..c91c42a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-interceptor-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-interceptor.svg b/ui/src/assets/icons/file-types/folder-interceptor.svg
new file mode 100644
index 0000000..e6cbf9f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-interceptor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-interface-open.svg b/ui/src/assets/icons/file-types/folder-interface-open.svg
new file mode 100644
index 0000000..ba54b0e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-interface-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-interface.svg b/ui/src/assets/icons/file-types/folder-interface.svg
new file mode 100644
index 0000000..993ce72
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-interface.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-ios-open.svg b/ui/src/assets/icons/file-types/folder-ios-open.svg
new file mode 100644
index 0000000..112fee6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ios-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-ios.svg b/ui/src/assets/icons/file-types/folder-ios.svg
new file mode 100644
index 0000000..7af3b85
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ios.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-java-open.svg b/ui/src/assets/icons/file-types/folder-java-open.svg
new file mode 100644
index 0000000..eb59229
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-java-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-java.svg b/ui/src/assets/icons/file-types/folder-java.svg
new file mode 100644
index 0000000..58fdd3d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-java.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-javascript-open.svg b/ui/src/assets/icons/file-types/folder-javascript-open.svg
new file mode 100644
index 0000000..581f3a2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-javascript-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-javascript.svg b/ui/src/assets/icons/file-types/folder-javascript.svg
new file mode 100644
index 0000000..97cf04c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-javascript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-jinja-open.svg b/ui/src/assets/icons/file-types/folder-jinja-open.svg
new file mode 100644
index 0000000..9c0b2b6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-jinja-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-jinja-open_light.svg b/ui/src/assets/icons/file-types/folder-jinja-open_light.svg
new file mode 100644
index 0000000..ffc940f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-jinja-open_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-jinja.svg b/ui/src/assets/icons/file-types/folder-jinja.svg
new file mode 100644
index 0000000..687efe3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-jinja.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-jinja_light.svg b/ui/src/assets/icons/file-types/folder-jinja_light.svg
new file mode 100644
index 0000000..c2b08bb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-jinja_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-job-open.svg b/ui/src/assets/icons/file-types/folder-job-open.svg
new file mode 100644
index 0000000..efd7cdf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-job-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-job.svg b/ui/src/assets/icons/file-types/folder-job.svg
new file mode 100644
index 0000000..9135aff
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-job.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-json-open.svg b/ui/src/assets/icons/file-types/folder-json-open.svg
new file mode 100644
index 0000000..29cdf2f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-json-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-json.svg b/ui/src/assets/icons/file-types/folder-json.svg
new file mode 100644
index 0000000..34085f6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-json.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-jupyter-open.svg b/ui/src/assets/icons/file-types/folder-jupyter-open.svg
new file mode 100644
index 0000000..d431953
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-jupyter-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-jupyter.svg b/ui/src/assets/icons/file-types/folder-jupyter.svg
new file mode 100644
index 0000000..d4d3eb3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-jupyter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-keys-open.svg b/ui/src/assets/icons/file-types/folder-keys-open.svg
new file mode 100644
index 0000000..783b16e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-keys-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-keys.svg b/ui/src/assets/icons/file-types/folder-keys.svg
new file mode 100644
index 0000000..3527f62
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-keys.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-kubernetes-open.svg b/ui/src/assets/icons/file-types/folder-kubernetes-open.svg
new file mode 100644
index 0000000..022be4d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-kubernetes-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-kubernetes.svg b/ui/src/assets/icons/file-types/folder-kubernetes.svg
new file mode 100644
index 0000000..b60d83d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-kubernetes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-kusto-open.svg b/ui/src/assets/icons/file-types/folder-kusto-open.svg
new file mode 100644
index 0000000..4ea80ca
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-kusto-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-kusto.svg b/ui/src/assets/icons/file-types/folder-kusto.svg
new file mode 100644
index 0000000..fa71096
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-kusto.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-layout-open.svg b/ui/src/assets/icons/file-types/folder-layout-open.svg
new file mode 100644
index 0000000..f8f1def
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-layout-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-layout.svg b/ui/src/assets/icons/file-types/folder-layout.svg
new file mode 100644
index 0000000..3d773bc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-layout.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-lefthook-open.svg b/ui/src/assets/icons/file-types/folder-lefthook-open.svg
new file mode 100644
index 0000000..a2694ba
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-lefthook-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-lefthook.svg b/ui/src/assets/icons/file-types/folder-lefthook.svg
new file mode 100644
index 0000000..0c7eb27
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-lefthook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-less-open.svg b/ui/src/assets/icons/file-types/folder-less-open.svg
new file mode 100644
index 0000000..3419b0a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-less-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-less.svg b/ui/src/assets/icons/file-types/folder-less.svg
new file mode 100644
index 0000000..b6abc5e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-less.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-lib-open.svg b/ui/src/assets/icons/file-types/folder-lib-open.svg
new file mode 100644
index 0000000..8c44431
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-lib-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-lib.svg b/ui/src/assets/icons/file-types/folder-lib.svg
new file mode 100644
index 0000000..4e75285
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-lib.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-link-open.svg b/ui/src/assets/icons/file-types/folder-link-open.svg
new file mode 100644
index 0000000..817d0d5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-link-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-link.svg b/ui/src/assets/icons/file-types/folder-link.svg
new file mode 100644
index 0000000..48a8bbe
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-linux-open.svg b/ui/src/assets/icons/file-types/folder-linux-open.svg
new file mode 100644
index 0000000..8517b35
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-linux-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-linux.svg b/ui/src/assets/icons/file-types/folder-linux.svg
new file mode 100644
index 0000000..df4d229
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-linux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-liquibase-open.svg b/ui/src/assets/icons/file-types/folder-liquibase-open.svg
new file mode 100644
index 0000000..2fe7ba6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-liquibase-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-liquibase.svg b/ui/src/assets/icons/file-types/folder-liquibase.svg
new file mode 100644
index 0000000..aea076a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-liquibase.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-log-open.svg b/ui/src/assets/icons/file-types/folder-log-open.svg
new file mode 100644
index 0000000..a78771e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-log-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-log.svg b/ui/src/assets/icons/file-types/folder-log.svg
new file mode 100644
index 0000000..b2ba6a5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-log.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-lottie-open.svg b/ui/src/assets/icons/file-types/folder-lottie-open.svg
new file mode 100644
index 0000000..adca025
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-lottie-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-lottie.svg b/ui/src/assets/icons/file-types/folder-lottie.svg
new file mode 100644
index 0000000..4d7fe34
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-lottie.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-lua-open.svg b/ui/src/assets/icons/file-types/folder-lua-open.svg
new file mode 100644
index 0000000..cb2ea6e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-lua-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-lua.svg b/ui/src/assets/icons/file-types/folder-lua.svg
new file mode 100644
index 0000000..e32819b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-lua.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-luau-open.svg b/ui/src/assets/icons/file-types/folder-luau-open.svg
new file mode 100644
index 0000000..2b113b4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-luau-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-luau.svg b/ui/src/assets/icons/file-types/folder-luau.svg
new file mode 100644
index 0000000..a6b4551
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-luau.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-macos-open.svg b/ui/src/assets/icons/file-types/folder-macos-open.svg
new file mode 100644
index 0000000..8d0280a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-macos-open.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-macos.svg b/ui/src/assets/icons/file-types/folder-macos.svg
new file mode 100644
index 0000000..6afe2ed
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-macos.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-mail-open.svg b/ui/src/assets/icons/file-types/folder-mail-open.svg
new file mode 100644
index 0000000..27774cf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mail-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mail.svg b/ui/src/assets/icons/file-types/folder-mail.svg
new file mode 100644
index 0000000..513e4b1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mail.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mappings-open.svg b/ui/src/assets/icons/file-types/folder-mappings-open.svg
new file mode 100644
index 0000000..510d06b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mappings-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mappings.svg b/ui/src/assets/icons/file-types/folder-mappings.svg
new file mode 100644
index 0000000..53b58e0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mappings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-markdown-open.svg b/ui/src/assets/icons/file-types/folder-markdown-open.svg
new file mode 100644
index 0000000..75ef904
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-markdown-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-markdown.svg b/ui/src/assets/icons/file-types/folder-markdown.svg
new file mode 100644
index 0000000..5df5d0a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-markdown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mercurial-open.svg b/ui/src/assets/icons/file-types/folder-mercurial-open.svg
new file mode 100644
index 0000000..74bbb9d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mercurial-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mercurial.svg b/ui/src/assets/icons/file-types/folder-mercurial.svg
new file mode 100644
index 0000000..5175b8e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mercurial.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-messages-open.svg b/ui/src/assets/icons/file-types/folder-messages-open.svg
new file mode 100644
index 0000000..2701529
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-messages-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-messages.svg b/ui/src/assets/icons/file-types/folder-messages.svg
new file mode 100644
index 0000000..ab3e2f8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-messages.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-meta-open.svg b/ui/src/assets/icons/file-types/folder-meta-open.svg
new file mode 100644
index 0000000..de1fd82
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-meta-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-meta.svg b/ui/src/assets/icons/file-types/folder-meta.svg
new file mode 100644
index 0000000..3a1b90a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-meta.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-middleware-open.svg b/ui/src/assets/icons/file-types/folder-middleware-open.svg
new file mode 100644
index 0000000..346954c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-middleware-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-middleware.svg b/ui/src/assets/icons/file-types/folder-middleware.svg
new file mode 100644
index 0000000..f12c99d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-middleware.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mjml-open.svg b/ui/src/assets/icons/file-types/folder-mjml-open.svg
new file mode 100644
index 0000000..81843f0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mjml-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mjml.svg b/ui/src/assets/icons/file-types/folder-mjml.svg
new file mode 100644
index 0000000..8d7f067
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mjml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mobile-open.svg b/ui/src/assets/icons/file-types/folder-mobile-open.svg
new file mode 100644
index 0000000..6a5a39b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mobile-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mobile.svg b/ui/src/assets/icons/file-types/folder-mobile.svg
new file mode 100644
index 0000000..03aab13
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mobile.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mock-open.svg b/ui/src/assets/icons/file-types/folder-mock-open.svg
new file mode 100644
index 0000000..c92929c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mock-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mock.svg b/ui/src/assets/icons/file-types/folder-mock.svg
new file mode 100644
index 0000000..22f88e5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mojo-open.svg b/ui/src/assets/icons/file-types/folder-mojo-open.svg
new file mode 100644
index 0000000..ce5b9be
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mojo-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-mojo.svg b/ui/src/assets/icons/file-types/folder-mojo.svg
new file mode 100644
index 0000000..67f7537
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-mojo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-molecule-open.svg b/ui/src/assets/icons/file-types/folder-molecule-open.svg
new file mode 100644
index 0000000..846e2f9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-molecule-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-molecule.svg b/ui/src/assets/icons/file-types/folder-molecule.svg
new file mode 100644
index 0000000..9c7905e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-molecule.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-moon-open.svg b/ui/src/assets/icons/file-types/folder-moon-open.svg
new file mode 100644
index 0000000..f2da8dd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-moon-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-moon.svg b/ui/src/assets/icons/file-types/folder-moon.svg
new file mode 100644
index 0000000..06613de
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-moon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-netlify-open.svg b/ui/src/assets/icons/file-types/folder-netlify-open.svg
new file mode 100644
index 0000000..d6f63b7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-netlify-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-netlify.svg b/ui/src/assets/icons/file-types/folder-netlify.svg
new file mode 100644
index 0000000..5473f42
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-netlify.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-next-open.svg b/ui/src/assets/icons/file-types/folder-next-open.svg
new file mode 100644
index 0000000..c8709ca
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-next-open.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-next.svg b/ui/src/assets/icons/file-types/folder-next.svg
new file mode 100644
index 0000000..cab1e8f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-next.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-ngrx-store-open.svg b/ui/src/assets/icons/file-types/folder-ngrx-store-open.svg
new file mode 100644
index 0000000..2c8514e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ngrx-store-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-ngrx-store.svg b/ui/src/assets/icons/file-types/folder-ngrx-store.svg
new file mode 100644
index 0000000..1f9cb2d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ngrx-store.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-node-open.svg b/ui/src/assets/icons/file-types/folder-node-open.svg
new file mode 100644
index 0000000..a785ed3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-node-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-node.svg b/ui/src/assets/icons/file-types/folder-node.svg
new file mode 100644
index 0000000..fb47492
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-node.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-nuxt-open.svg b/ui/src/assets/icons/file-types/folder-nuxt-open.svg
new file mode 100644
index 0000000..c49ff8d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-nuxt-open.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-nuxt.svg b/ui/src/assets/icons/file-types/folder-nuxt.svg
new file mode 100644
index 0000000..a0a52b0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-nuxt.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-obsidian-open.svg b/ui/src/assets/icons/file-types/folder-obsidian-open.svg
new file mode 100644
index 0000000..f7d1305
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-obsidian-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-obsidian.svg b/ui/src/assets/icons/file-types/folder-obsidian.svg
new file mode 100644
index 0000000..cd16a52
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-obsidian.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-open.svg b/ui/src/assets/icons/file-types/folder-open.svg
new file mode 100644
index 0000000..eac8918
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-open.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-organism-open.svg b/ui/src/assets/icons/file-types/folder-organism-open.svg
new file mode 100644
index 0000000..6be44d2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-organism-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-organism.svg b/ui/src/assets/icons/file-types/folder-organism.svg
new file mode 100644
index 0000000..50092a0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-organism.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-other-open.svg b/ui/src/assets/icons/file-types/folder-other-open.svg
new file mode 100644
index 0000000..ea4144f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-other-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-other.svg b/ui/src/assets/icons/file-types/folder-other.svg
new file mode 100644
index 0000000..df3d27f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-other.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-packages-open.svg b/ui/src/assets/icons/file-types/folder-packages-open.svg
new file mode 100644
index 0000000..7ac6075
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-packages-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-packages.svg b/ui/src/assets/icons/file-types/folder-packages.svg
new file mode 100644
index 0000000..9ba67cb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-packages.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-pdf-open.svg b/ui/src/assets/icons/file-types/folder-pdf-open.svg
new file mode 100644
index 0000000..fdeccb0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-pdf-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-pdf.svg b/ui/src/assets/icons/file-types/folder-pdf.svg
new file mode 100644
index 0000000..db0ace7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-pdf.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-pdm-open.svg b/ui/src/assets/icons/file-types/folder-pdm-open.svg
new file mode 100644
index 0000000..6145f79
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-pdm-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-pdm.svg b/ui/src/assets/icons/file-types/folder-pdm.svg
new file mode 100644
index 0000000..9508547
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-pdm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-php-open.svg b/ui/src/assets/icons/file-types/folder-php-open.svg
new file mode 100644
index 0000000..2059a9b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-php-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-php.svg b/ui/src/assets/icons/file-types/folder-php.svg
new file mode 100644
index 0000000..4304e17
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-php.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-phpmailer-open.svg b/ui/src/assets/icons/file-types/folder-phpmailer-open.svg
new file mode 100644
index 0000000..26388bb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-phpmailer-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-phpmailer.svg b/ui/src/assets/icons/file-types/folder-phpmailer.svg
new file mode 100644
index 0000000..18f696c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-phpmailer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-pipe-open.svg b/ui/src/assets/icons/file-types/folder-pipe-open.svg
new file mode 100644
index 0000000..8aacef0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-pipe-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-pipe.svg b/ui/src/assets/icons/file-types/folder-pipe.svg
new file mode 100644
index 0000000..9ba5d0a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-pipe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-plastic-open.svg b/ui/src/assets/icons/file-types/folder-plastic-open.svg
new file mode 100644
index 0000000..b93a541
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-plastic-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-plastic.svg b/ui/src/assets/icons/file-types/folder-plastic.svg
new file mode 100644
index 0000000..5e595f3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-plastic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-plugin-open.svg b/ui/src/assets/icons/file-types/folder-plugin-open.svg
new file mode 100644
index 0000000..5a7f03a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-plugin-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-plugin.svg b/ui/src/assets/icons/file-types/folder-plugin.svg
new file mode 100644
index 0000000..14a3154
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-plugin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-policy-open.svg b/ui/src/assets/icons/file-types/folder-policy-open.svg
new file mode 100644
index 0000000..c2b51d4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-policy-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-policy.svg b/ui/src/assets/icons/file-types/folder-policy.svg
new file mode 100644
index 0000000..1b1781d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-policy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-powershell-open.svg b/ui/src/assets/icons/file-types/folder-powershell-open.svg
new file mode 100644
index 0000000..be4b458
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-powershell-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-powershell.svg b/ui/src/assets/icons/file-types/folder-powershell.svg
new file mode 100644
index 0000000..6f28098
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-powershell.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-prisma-open.svg b/ui/src/assets/icons/file-types/folder-prisma-open.svg
new file mode 100644
index 0000000..95df8ba
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-prisma-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-prisma.svg b/ui/src/assets/icons/file-types/folder-prisma.svg
new file mode 100644
index 0000000..a166ebd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-prisma.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-private-open.svg b/ui/src/assets/icons/file-types/folder-private-open.svg
new file mode 100644
index 0000000..19094be
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-private-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-private.svg b/ui/src/assets/icons/file-types/folder-private.svg
new file mode 100644
index 0000000..da95ece
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-private.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-project-open.svg b/ui/src/assets/icons/file-types/folder-project-open.svg
new file mode 100644
index 0000000..9da2862
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-project-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-project.svg b/ui/src/assets/icons/file-types/folder-project.svg
new file mode 100644
index 0000000..f575aa0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-project.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-prompts-open.svg b/ui/src/assets/icons/file-types/folder-prompts-open.svg
new file mode 100644
index 0000000..5ed3346
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-prompts-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-prompts.svg b/ui/src/assets/icons/file-types/folder-prompts.svg
new file mode 100644
index 0000000..969535b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-prompts.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-proto-open.svg b/ui/src/assets/icons/file-types/folder-proto-open.svg
new file mode 100644
index 0000000..710de39
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-proto-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-proto.svg b/ui/src/assets/icons/file-types/folder-proto.svg
new file mode 100644
index 0000000..935fcbc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-proto.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-public-open.svg b/ui/src/assets/icons/file-types/folder-public-open.svg
new file mode 100644
index 0000000..04449ed
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-public-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-public.svg b/ui/src/assets/icons/file-types/folder-public.svg
new file mode 100644
index 0000000..ea59939
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-public.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-python-open.svg b/ui/src/assets/icons/file-types/folder-python-open.svg
new file mode 100644
index 0000000..dbfc367
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-python-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-python.svg b/ui/src/assets/icons/file-types/folder-python.svg
new file mode 100644
index 0000000..aae0736
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-python.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-pytorch-open.svg b/ui/src/assets/icons/file-types/folder-pytorch-open.svg
new file mode 100644
index 0000000..46f664f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-pytorch-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-pytorch.svg b/ui/src/assets/icons/file-types/folder-pytorch.svg
new file mode 100644
index 0000000..2616b6b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-pytorch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-quasar-open.svg b/ui/src/assets/icons/file-types/folder-quasar-open.svg
new file mode 100644
index 0000000..5fb6b92
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-quasar-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-quasar.svg b/ui/src/assets/icons/file-types/folder-quasar.svg
new file mode 100644
index 0000000..b098014
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-quasar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-queue-open.svg b/ui/src/assets/icons/file-types/folder-queue-open.svg
new file mode 100644
index 0000000..5afa821
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-queue-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-queue.svg b/ui/src/assets/icons/file-types/folder-queue.svg
new file mode 100644
index 0000000..2445304
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-queue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-react-components-open.svg b/ui/src/assets/icons/file-types/folder-react-components-open.svg
new file mode 100644
index 0000000..05af544
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-react-components-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-react-components.svg b/ui/src/assets/icons/file-types/folder-react-components.svg
new file mode 100644
index 0000000..5f117a7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-react-components.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-redux-reducer-open.svg b/ui/src/assets/icons/file-types/folder-redux-reducer-open.svg
new file mode 100644
index 0000000..838bf52
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-redux-reducer-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-redux-reducer.svg b/ui/src/assets/icons/file-types/folder-redux-reducer.svg
new file mode 100644
index 0000000..a3b441f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-redux-reducer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-repository-open.svg b/ui/src/assets/icons/file-types/folder-repository-open.svg
new file mode 100644
index 0000000..9c6275d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-repository-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-repository.svg b/ui/src/assets/icons/file-types/folder-repository.svg
new file mode 100644
index 0000000..4f75206
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-repository.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-resolver-open.svg b/ui/src/assets/icons/file-types/folder-resolver-open.svg
new file mode 100644
index 0000000..5a4b752
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-resolver-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-resolver.svg b/ui/src/assets/icons/file-types/folder-resolver.svg
new file mode 100644
index 0000000..c59a6b4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-resolver.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-resource-open.svg b/ui/src/assets/icons/file-types/folder-resource-open.svg
new file mode 100644
index 0000000..0f534e1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-resource-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-resource.svg b/ui/src/assets/icons/file-types/folder-resource.svg
new file mode 100644
index 0000000..24a053a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-resource.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-review-open.svg b/ui/src/assets/icons/file-types/folder-review-open.svg
new file mode 100644
index 0000000..2384601
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-review-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-review.svg b/ui/src/assets/icons/file-types/folder-review.svg
new file mode 100644
index 0000000..c7b138c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-review.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-robot-open.svg b/ui/src/assets/icons/file-types/folder-robot-open.svg
new file mode 100644
index 0000000..cd501c4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-robot-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-robot.svg b/ui/src/assets/icons/file-types/folder-robot.svg
new file mode 100644
index 0000000..fa582f4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-robot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-routes-open.svg b/ui/src/assets/icons/file-types/folder-routes-open.svg
new file mode 100644
index 0000000..c9c875e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-routes-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-routes.svg b/ui/src/assets/icons/file-types/folder-routes.svg
new file mode 100644
index 0000000..2fb204d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-routes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-rules-open.svg b/ui/src/assets/icons/file-types/folder-rules-open.svg
new file mode 100644
index 0000000..1f9c01f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-rules-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-rules.svg b/ui/src/assets/icons/file-types/folder-rules.svg
new file mode 100644
index 0000000..baa5b61
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-rules.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-rust-open.svg b/ui/src/assets/icons/file-types/folder-rust-open.svg
new file mode 100644
index 0000000..65be154
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-rust-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-rust.svg b/ui/src/assets/icons/file-types/folder-rust.svg
new file mode 100644
index 0000000..afe65f6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-rust.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-sandbox-open.svg b/ui/src/assets/icons/file-types/folder-sandbox-open.svg
new file mode 100644
index 0000000..e0c7a06
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-sandbox-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-sandbox.svg b/ui/src/assets/icons/file-types/folder-sandbox.svg
new file mode 100644
index 0000000..4339173
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-sandbox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-sass-open.svg b/ui/src/assets/icons/file-types/folder-sass-open.svg
new file mode 100644
index 0000000..0a2a82e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-sass-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-sass.svg b/ui/src/assets/icons/file-types/folder-sass.svg
new file mode 100644
index 0000000..6f28731
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-sass.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-scala-open.svg b/ui/src/assets/icons/file-types/folder-scala-open.svg
new file mode 100644
index 0000000..fb4aee7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-scala-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-scala.svg b/ui/src/assets/icons/file-types/folder-scala.svg
new file mode 100644
index 0000000..d78a074
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-scala.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-scons-open.svg b/ui/src/assets/icons/file-types/folder-scons-open.svg
new file mode 100644
index 0000000..db89612
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-scons-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-scons.svg b/ui/src/assets/icons/file-types/folder-scons.svg
new file mode 100644
index 0000000..aae02b4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-scons.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-scripts-open.svg b/ui/src/assets/icons/file-types/folder-scripts-open.svg
new file mode 100644
index 0000000..981a43f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-scripts-open.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-scripts.svg b/ui/src/assets/icons/file-types/folder-scripts.svg
new file mode 100644
index 0000000..4b755ac
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-scripts.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-secure-open.svg b/ui/src/assets/icons/file-types/folder-secure-open.svg
new file mode 100644
index 0000000..163f7da
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-secure-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-secure.svg b/ui/src/assets/icons/file-types/folder-secure.svg
new file mode 100644
index 0000000..110093f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-secure.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-seeders-open.svg b/ui/src/assets/icons/file-types/folder-seeders-open.svg
new file mode 100644
index 0000000..b931940
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-seeders-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-seeders.svg b/ui/src/assets/icons/file-types/folder-seeders.svg
new file mode 100644
index 0000000..cd59776
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-seeders.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-server-open.svg b/ui/src/assets/icons/file-types/folder-server-open.svg
new file mode 100644
index 0000000..706b8af
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-server-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-server.svg b/ui/src/assets/icons/file-types/folder-server.svg
new file mode 100644
index 0000000..4f03f47
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-server.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-serverless-open.svg b/ui/src/assets/icons/file-types/folder-serverless-open.svg
new file mode 100644
index 0000000..113f73c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-serverless-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-serverless.svg b/ui/src/assets/icons/file-types/folder-serverless.svg
new file mode 100644
index 0000000..226f89d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-serverless.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-shader-open.svg b/ui/src/assets/icons/file-types/folder-shader-open.svg
new file mode 100644
index 0000000..03e00ed
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-shader-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-shader.svg b/ui/src/assets/icons/file-types/folder-shader.svg
new file mode 100644
index 0000000..57772b3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-shader.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-shared-open.svg b/ui/src/assets/icons/file-types/folder-shared-open.svg
new file mode 100644
index 0000000..6542e7f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-shared-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-shared.svg b/ui/src/assets/icons/file-types/folder-shared.svg
new file mode 100644
index 0000000..01e7a17
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-shared.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-snapcraft-open.svg b/ui/src/assets/icons/file-types/folder-snapcraft-open.svg
new file mode 100644
index 0000000..1a03068
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-snapcraft-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-snapcraft.svg b/ui/src/assets/icons/file-types/folder-snapcraft.svg
new file mode 100644
index 0000000..fc77b78
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-snapcraft.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-snippet-open.svg b/ui/src/assets/icons/file-types/folder-snippet-open.svg
new file mode 100644
index 0000000..451c291
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-snippet-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-snippet.svg b/ui/src/assets/icons/file-types/folder-snippet.svg
new file mode 100644
index 0000000..991f5c4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-snippet.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-src-open.svg b/ui/src/assets/icons/file-types/folder-src-open.svg
new file mode 100644
index 0000000..8cd9ee3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-src-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-src-tauri-open.svg b/ui/src/assets/icons/file-types/folder-src-tauri-open.svg
new file mode 100644
index 0000000..969c577
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-src-tauri-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-src-tauri.svg b/ui/src/assets/icons/file-types/folder-src-tauri.svg
new file mode 100644
index 0000000..727790c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-src-tauri.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-src.svg b/ui/src/assets/icons/file-types/folder-src.svg
new file mode 100644
index 0000000..8d45da9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-src.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-stack-open.svg b/ui/src/assets/icons/file-types/folder-stack-open.svg
new file mode 100644
index 0000000..cfd8bd0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-stack-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-stack.svg b/ui/src/assets/icons/file-types/folder-stack.svg
new file mode 100644
index 0000000..9c0b10d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-stack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-stencil-open.svg b/ui/src/assets/icons/file-types/folder-stencil-open.svg
new file mode 100644
index 0000000..6dea078
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-stencil-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-stencil.svg b/ui/src/assets/icons/file-types/folder-stencil.svg
new file mode 100644
index 0000000..c0443c9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-stencil.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-store-open.svg b/ui/src/assets/icons/file-types/folder-store-open.svg
new file mode 100644
index 0000000..13e415b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-store-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-store.svg b/ui/src/assets/icons/file-types/folder-store.svg
new file mode 100644
index 0000000..ae29c03
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-store.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-storybook-open.svg b/ui/src/assets/icons/file-types/folder-storybook-open.svg
new file mode 100644
index 0000000..9be24b2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-storybook-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-storybook.svg b/ui/src/assets/icons/file-types/folder-storybook.svg
new file mode 100644
index 0000000..26e6246
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-storybook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-stylus-open.svg b/ui/src/assets/icons/file-types/folder-stylus-open.svg
new file mode 100644
index 0000000..9615173
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-stylus-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-stylus.svg b/ui/src/assets/icons/file-types/folder-stylus.svg
new file mode 100644
index 0000000..68ae158
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-stylus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-sublime-open.svg b/ui/src/assets/icons/file-types/folder-sublime-open.svg
new file mode 100644
index 0000000..5066f3a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-sublime-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-sublime.svg b/ui/src/assets/icons/file-types/folder-sublime.svg
new file mode 100644
index 0000000..1361eda
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-sublime.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-supabase-open.svg b/ui/src/assets/icons/file-types/folder-supabase-open.svg
new file mode 100644
index 0000000..d58a692
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-supabase-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-supabase.svg b/ui/src/assets/icons/file-types/folder-supabase.svg
new file mode 100644
index 0000000..c0c8189
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-supabase.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-svelte-open.svg b/ui/src/assets/icons/file-types/folder-svelte-open.svg
new file mode 100644
index 0000000..f72ae2f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-svelte-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-svelte.svg b/ui/src/assets/icons/file-types/folder-svelte.svg
new file mode 100644
index 0000000..61bf1d4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-svelte.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-svg-open.svg b/ui/src/assets/icons/file-types/folder-svg-open.svg
new file mode 100644
index 0000000..f8ef72b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-svg-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-svg.svg b/ui/src/assets/icons/file-types/folder-svg.svg
new file mode 100644
index 0000000..320b9eb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-svg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-syntax-open.svg b/ui/src/assets/icons/file-types/folder-syntax-open.svg
new file mode 100644
index 0000000..fd9d972
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-syntax-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-syntax.svg b/ui/src/assets/icons/file-types/folder-syntax.svg
new file mode 100644
index 0000000..be4ab16
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-syntax.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-target-open.svg b/ui/src/assets/icons/file-types/folder-target-open.svg
new file mode 100644
index 0000000..0004bf8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-target-open.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-target.svg b/ui/src/assets/icons/file-types/folder-target.svg
new file mode 100644
index 0000000..5872750
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-target.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-taskfile-open.svg b/ui/src/assets/icons/file-types/folder-taskfile-open.svg
new file mode 100644
index 0000000..fc2c501
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-taskfile-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-taskfile.svg b/ui/src/assets/icons/file-types/folder-taskfile.svg
new file mode 100644
index 0000000..1a3cac7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-taskfile.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-tasks-open.svg b/ui/src/assets/icons/file-types/folder-tasks-open.svg
new file mode 100644
index 0000000..ed0e67f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-tasks-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-tasks.svg b/ui/src/assets/icons/file-types/folder-tasks.svg
new file mode 100644
index 0000000..1a9ef8a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-tasks.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-television-open.svg b/ui/src/assets/icons/file-types/folder-television-open.svg
new file mode 100644
index 0000000..33c21d8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-television-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-television.svg b/ui/src/assets/icons/file-types/folder-television.svg
new file mode 100644
index 0000000..dc10294
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-television.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-temp-open.svg b/ui/src/assets/icons/file-types/folder-temp-open.svg
new file mode 100644
index 0000000..ec798b1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-temp-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-temp.svg b/ui/src/assets/icons/file-types/folder-temp.svg
new file mode 100644
index 0000000..3002a86
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-temp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-template-open.svg b/ui/src/assets/icons/file-types/folder-template-open.svg
new file mode 100644
index 0000000..e3f822b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-template-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-template.svg b/ui/src/assets/icons/file-types/folder-template.svg
new file mode 100644
index 0000000..1d15837
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-template.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-terraform-open.svg b/ui/src/assets/icons/file-types/folder-terraform-open.svg
new file mode 100644
index 0000000..fff197b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-terraform-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-terraform.svg b/ui/src/assets/icons/file-types/folder-terraform.svg
new file mode 100644
index 0000000..e71fba8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-terraform.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-test-open.svg b/ui/src/assets/icons/file-types/folder-test-open.svg
new file mode 100644
index 0000000..f3fefb3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-test-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-test.svg b/ui/src/assets/icons/file-types/folder-test.svg
new file mode 100644
index 0000000..92bee16
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-test.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-theme-open.svg b/ui/src/assets/icons/file-types/folder-theme-open.svg
new file mode 100644
index 0000000..5e79f99
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-theme-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-theme.svg b/ui/src/assets/icons/file-types/folder-theme.svg
new file mode 100644
index 0000000..88efa95
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-theme.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-tools-open.svg b/ui/src/assets/icons/file-types/folder-tools-open.svg
new file mode 100644
index 0000000..77ecaa8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-tools-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-tools.svg b/ui/src/assets/icons/file-types/folder-tools.svg
new file mode 100644
index 0000000..d591a1f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-tools.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-trash-open.svg b/ui/src/assets/icons/file-types/folder-trash-open.svg
new file mode 100644
index 0000000..add51b8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-trash-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-trash.svg b/ui/src/assets/icons/file-types/folder-trash.svg
new file mode 100644
index 0000000..1e81d28
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-trash.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-trigger-open.svg b/ui/src/assets/icons/file-types/folder-trigger-open.svg
new file mode 100644
index 0000000..ecd80d3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-trigger-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-trigger.svg b/ui/src/assets/icons/file-types/folder-trigger.svg
new file mode 100644
index 0000000..cfe23c1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-trigger.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-turborepo-open.svg b/ui/src/assets/icons/file-types/folder-turborepo-open.svg
new file mode 100644
index 0000000..e0d7c35
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-turborepo-open.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-turborepo.svg b/ui/src/assets/icons/file-types/folder-turborepo.svg
new file mode 100644
index 0000000..ea20336
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-turborepo.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-typescript-open.svg b/ui/src/assets/icons/file-types/folder-typescript-open.svg
new file mode 100644
index 0000000..87c8e2f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-typescript-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-typescript.svg b/ui/src/assets/icons/file-types/folder-typescript.svg
new file mode 100644
index 0000000..df26f89
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-typescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-ui-open.svg b/ui/src/assets/icons/file-types/folder-ui-open.svg
new file mode 100644
index 0000000..3044916
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ui-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-ui.svg b/ui/src/assets/icons/file-types/folder-ui.svg
new file mode 100644
index 0000000..fa320d1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-ui.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-unity-open.svg b/ui/src/assets/icons/file-types/folder-unity-open.svg
new file mode 100644
index 0000000..cb036d5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-unity-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-unity.svg b/ui/src/assets/icons/file-types/folder-unity.svg
new file mode 100644
index 0000000..c751de2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-unity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-update-open.svg b/ui/src/assets/icons/file-types/folder-update-open.svg
new file mode 100644
index 0000000..a6d18a9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-update-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-update.svg b/ui/src/assets/icons/file-types/folder-update.svg
new file mode 100644
index 0000000..65eaf57
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-update.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-upload-open.svg b/ui/src/assets/icons/file-types/folder-upload-open.svg
new file mode 100644
index 0000000..24fc359
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-upload-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-upload.svg b/ui/src/assets/icons/file-types/folder-upload.svg
new file mode 100644
index 0000000..423c6c1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-upload.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-utils-open.svg b/ui/src/assets/icons/file-types/folder-utils-open.svg
new file mode 100644
index 0000000..b894eff
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-utils-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-utils.svg b/ui/src/assets/icons/file-types/folder-utils.svg
new file mode 100644
index 0000000..fcc7999
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-utils.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vercel-open.svg b/ui/src/assets/icons/file-types/folder-vercel-open.svg
new file mode 100644
index 0000000..c571c63
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vercel-open.svg
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-vercel.svg b/ui/src/assets/icons/file-types/folder-vercel.svg
new file mode 100644
index 0000000..5138481
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vercel.svg
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/folder-verdaccio-open.svg b/ui/src/assets/icons/file-types/folder-verdaccio-open.svg
new file mode 100644
index 0000000..24beac5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-verdaccio-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-verdaccio.svg b/ui/src/assets/icons/file-types/folder-verdaccio.svg
new file mode 100644
index 0000000..8e78ba7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-verdaccio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-video-open.svg b/ui/src/assets/icons/file-types/folder-video-open.svg
new file mode 100644
index 0000000..ea60cd0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-video-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-video.svg b/ui/src/assets/icons/file-types/folder-video.svg
new file mode 100644
index 0000000..d138554
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-video.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-views-open.svg b/ui/src/assets/icons/file-types/folder-views-open.svg
new file mode 100644
index 0000000..1c785e4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-views-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-views.svg b/ui/src/assets/icons/file-types/folder-views.svg
new file mode 100644
index 0000000..5d41f10
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-views.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vm-open.svg b/ui/src/assets/icons/file-types/folder-vm-open.svg
new file mode 100644
index 0000000..e1a2b54
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vm-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vm.svg b/ui/src/assets/icons/file-types/folder-vm.svg
new file mode 100644
index 0000000..1ee3a95
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vscode-open.svg b/ui/src/assets/icons/file-types/folder-vscode-open.svg
new file mode 100644
index 0000000..82e3a21
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vscode-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vscode.svg b/ui/src/assets/icons/file-types/folder-vscode.svg
new file mode 100644
index 0000000..07ccbd6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vscode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vue-directives-open.svg b/ui/src/assets/icons/file-types/folder-vue-directives-open.svg
new file mode 100644
index 0000000..341354b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vue-directives-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vue-directives.svg b/ui/src/assets/icons/file-types/folder-vue-directives.svg
new file mode 100644
index 0000000..fc28ccb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vue-directives.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vue-open.svg b/ui/src/assets/icons/file-types/folder-vue-open.svg
new file mode 100644
index 0000000..03abcaf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vue-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vue.svg b/ui/src/assets/icons/file-types/folder-vue.svg
new file mode 100644
index 0000000..c7cf38e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vuepress-open.svg b/ui/src/assets/icons/file-types/folder-vuepress-open.svg
new file mode 100644
index 0000000..af2b09b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vuepress-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vuepress.svg b/ui/src/assets/icons/file-types/folder-vuepress.svg
new file mode 100644
index 0000000..42fb0dc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vuepress.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vuex-store-open.svg b/ui/src/assets/icons/file-types/folder-vuex-store-open.svg
new file mode 100644
index 0000000..77c3c46
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vuex-store-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-vuex-store.svg b/ui/src/assets/icons/file-types/folder-vuex-store.svg
new file mode 100644
index 0000000..5c6793e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-vuex-store.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-wakatime-open.svg b/ui/src/assets/icons/file-types/folder-wakatime-open.svg
new file mode 100644
index 0000000..d1dbc38
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-wakatime-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-wakatime.svg b/ui/src/assets/icons/file-types/folder-wakatime.svg
new file mode 100644
index 0000000..860a661
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-wakatime.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-webpack-open.svg b/ui/src/assets/icons/file-types/folder-webpack-open.svg
new file mode 100644
index 0000000..acd1e19
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-webpack-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-webpack.svg b/ui/src/assets/icons/file-types/folder-webpack.svg
new file mode 100644
index 0000000..3ac887a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-webpack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-windows-open.svg b/ui/src/assets/icons/file-types/folder-windows-open.svg
new file mode 100644
index 0000000..9173ff9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-windows-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-windows.svg b/ui/src/assets/icons/file-types/folder-windows.svg
new file mode 100644
index 0000000..184de31
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-windows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-wordpress-open.svg b/ui/src/assets/icons/file-types/folder-wordpress-open.svg
new file mode 100644
index 0000000..8cb4006
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-wordpress-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-wordpress.svg b/ui/src/assets/icons/file-types/folder-wordpress.svg
new file mode 100644
index 0000000..a954a2b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-wordpress.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-yarn-open.svg b/ui/src/assets/icons/file-types/folder-yarn-open.svg
new file mode 100644
index 0000000..ddbb988
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-yarn-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-yarn.svg b/ui/src/assets/icons/file-types/folder-yarn.svg
new file mode 100644
index 0000000..58aee64
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-yarn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-zeabur-open.svg b/ui/src/assets/icons/file-types/folder-zeabur-open.svg
new file mode 100644
index 0000000..ac2a31a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-zeabur-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder-zeabur.svg b/ui/src/assets/icons/file-types/folder-zeabur.svg
new file mode 100644
index 0000000..b0b8421
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder-zeabur.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/folder.svg b/ui/src/assets/icons/file-types/folder.svg
new file mode 100644
index 0000000..97ee81c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/folder.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/ui/src/assets/icons/file-types/font.svg b/ui/src/assets/icons/file-types/font.svg
new file mode 100644
index 0000000..961586d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/font.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/forth.svg b/ui/src/assets/icons/file-types/forth.svg
new file mode 100644
index 0000000..50b66af
--- /dev/null
+++ b/ui/src/assets/icons/file-types/forth.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/fortran.svg b/ui/src/assets/icons/file-types/fortran.svg
new file mode 100644
index 0000000..235db1a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/fortran.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/foxpro.svg b/ui/src/assets/icons/file-types/foxpro.svg
new file mode 100644
index 0000000..e2d5eb0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/foxpro.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/freemarker.svg b/ui/src/assets/icons/file-types/freemarker.svg
new file mode 100644
index 0000000..edf98f6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/freemarker.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/fsharp.svg b/ui/src/assets/icons/file-types/fsharp.svg
new file mode 100644
index 0000000..1e5b7cf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/fsharp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/fusebox.svg b/ui/src/assets/icons/file-types/fusebox.svg
new file mode 100644
index 0000000..a4ad3d6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/fusebox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gamemaker.svg b/ui/src/assets/icons/file-types/gamemaker.svg
new file mode 100644
index 0000000..4097cdd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gamemaker.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/garden.svg b/ui/src/assets/icons/file-types/garden.svg
new file mode 100644
index 0000000..a96386d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/garden.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gatsby.svg b/ui/src/assets/icons/file-types/gatsby.svg
new file mode 100644
index 0000000..c267469
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gatsby.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gcp.svg b/ui/src/assets/icons/file-types/gcp.svg
new file mode 100644
index 0000000..62be904
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gcp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gemfile.svg b/ui/src/assets/icons/file-types/gemfile.svg
new file mode 100644
index 0000000..757c89d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gemfile.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gemini-ai.svg b/ui/src/assets/icons/file-types/gemini-ai.svg
new file mode 100644
index 0000000..0911694
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gemini-ai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gemini.svg b/ui/src/assets/icons/file-types/gemini.svg
new file mode 100644
index 0000000..79ad4bf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gemini.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/git.svg b/ui/src/assets/icons/file-types/git.svg
new file mode 100644
index 0000000..c1e08fd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/git.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/github-actions-workflow.svg b/ui/src/assets/icons/file-types/github-actions-workflow.svg
new file mode 100644
index 0000000..1c724c5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/github-actions-workflow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/github-sponsors.svg b/ui/src/assets/icons/file-types/github-sponsors.svg
new file mode 100644
index 0000000..72fb668
--- /dev/null
+++ b/ui/src/assets/icons/file-types/github-sponsors.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gitlab.svg b/ui/src/assets/icons/file-types/gitlab.svg
new file mode 100644
index 0000000..ceeabaf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gitlab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gitpod.svg b/ui/src/assets/icons/file-types/gitpod.svg
new file mode 100644
index 0000000..a992017
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gitpod.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gleam.svg b/ui/src/assets/icons/file-types/gleam.svg
new file mode 100644
index 0000000..76e0d0c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gleam.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gnuplot.svg b/ui/src/assets/icons/file-types/gnuplot.svg
new file mode 100644
index 0000000..8cc510b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gnuplot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/go-mod.svg b/ui/src/assets/icons/file-types/go-mod.svg
new file mode 100644
index 0000000..1689116
--- /dev/null
+++ b/ui/src/assets/icons/file-types/go-mod.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/go.svg b/ui/src/assets/icons/file-types/go.svg
new file mode 100644
index 0000000..d874e32
--- /dev/null
+++ b/ui/src/assets/icons/file-types/go.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/go_gopher.svg b/ui/src/assets/icons/file-types/go_gopher.svg
new file mode 100644
index 0000000..e465f74
--- /dev/null
+++ b/ui/src/assets/icons/file-types/go_gopher.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/godot-assets.svg b/ui/src/assets/icons/file-types/godot-assets.svg
new file mode 100644
index 0000000..19e193d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/godot-assets.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/godot.svg b/ui/src/assets/icons/file-types/godot.svg
new file mode 100644
index 0000000..4b1dd7f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/godot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gradle.svg b/ui/src/assets/icons/file-types/gradle.svg
new file mode 100644
index 0000000..72d88fd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gradle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/grafana-alloy.svg b/ui/src/assets/icons/file-types/grafana-alloy.svg
new file mode 100644
index 0000000..cf00031
--- /dev/null
+++ b/ui/src/assets/icons/file-types/grafana-alloy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/grain.svg b/ui/src/assets/icons/file-types/grain.svg
new file mode 100644
index 0000000..f96d46b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/grain.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/graphcool.svg b/ui/src/assets/icons/file-types/graphcool.svg
new file mode 100644
index 0000000..bdaedb9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/graphcool.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/graphql.svg b/ui/src/assets/icons/file-types/graphql.svg
new file mode 100644
index 0000000..252b0f7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/graphql.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gridsome.svg b/ui/src/assets/icons/file-types/gridsome.svg
new file mode 100644
index 0000000..8727741
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gridsome.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/groovy.svg b/ui/src/assets/icons/file-types/groovy.svg
new file mode 100644
index 0000000..9af0c08
--- /dev/null
+++ b/ui/src/assets/icons/file-types/groovy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/grunt.svg b/ui/src/assets/icons/file-types/grunt.svg
new file mode 100644
index 0000000..2b14994
--- /dev/null
+++ b/ui/src/assets/icons/file-types/grunt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/gulp.svg b/ui/src/assets/icons/file-types/gulp.svg
new file mode 100644
index 0000000..bc6a77f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/gulp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/h.svg b/ui/src/assets/icons/file-types/h.svg
new file mode 100644
index 0000000..08db8fe
--- /dev/null
+++ b/ui/src/assets/icons/file-types/h.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hack.svg b/ui/src/assets/icons/file-types/hack.svg
new file mode 100644
index 0000000..921cd73
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hadolint.svg b/ui/src/assets/icons/file-types/hadolint.svg
new file mode 100644
index 0000000..26195f5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hadolint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/haml.svg b/ui/src/assets/icons/file-types/haml.svg
new file mode 100644
index 0000000..bf08db5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/haml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/handlebars.svg b/ui/src/assets/icons/file-types/handlebars.svg
new file mode 100644
index 0000000..cf89930
--- /dev/null
+++ b/ui/src/assets/icons/file-types/handlebars.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hardhat.svg b/ui/src/assets/icons/file-types/hardhat.svg
new file mode 100644
index 0000000..dad8d45
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hardhat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/harmonix.svg b/ui/src/assets/icons/file-types/harmonix.svg
new file mode 100644
index 0000000..299fa47
--- /dev/null
+++ b/ui/src/assets/icons/file-types/harmonix.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/haskell.svg b/ui/src/assets/icons/file-types/haskell.svg
new file mode 100644
index 0000000..ae44927
--- /dev/null
+++ b/ui/src/assets/icons/file-types/haskell.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/haxe.svg b/ui/src/assets/icons/file-types/haxe.svg
new file mode 100644
index 0000000..cb28364
--- /dev/null
+++ b/ui/src/assets/icons/file-types/haxe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hcl.svg b/ui/src/assets/icons/file-types/hcl.svg
new file mode 100644
index 0000000..71edfb4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hcl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hcl_light.svg b/ui/src/assets/icons/file-types/hcl_light.svg
new file mode 100644
index 0000000..0196914
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hcl_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/helm.svg b/ui/src/assets/icons/file-types/helm.svg
new file mode 100644
index 0000000..58aa4a8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/helm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/heroku.svg b/ui/src/assets/icons/file-types/heroku.svg
new file mode 100644
index 0000000..d9d1ab0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/heroku.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hex.svg b/ui/src/assets/icons/file-types/hex.svg
new file mode 100644
index 0000000..e50c677
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hex.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/histoire.svg b/ui/src/assets/icons/file-types/histoire.svg
new file mode 100644
index 0000000..5619c16
--- /dev/null
+++ b/ui/src/assets/icons/file-types/histoire.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hjson.svg b/ui/src/assets/icons/file-types/hjson.svg
new file mode 100644
index 0000000..7725feb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hjson.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/horusec.svg b/ui/src/assets/icons/file-types/horusec.svg
new file mode 100644
index 0000000..9ea1155
--- /dev/null
+++ b/ui/src/assets/icons/file-types/horusec.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hosts.svg b/ui/src/assets/icons/file-types/hosts.svg
new file mode 100644
index 0000000..f88e7c6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hosts.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hosts_light.svg b/ui/src/assets/icons/file-types/hosts_light.svg
new file mode 100644
index 0000000..613a25e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hosts_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hpp.svg b/ui/src/assets/icons/file-types/hpp.svg
new file mode 100644
index 0000000..3e6872d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hpp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/html.svg b/ui/src/assets/icons/file-types/html.svg
new file mode 100644
index 0000000..71caf32
--- /dev/null
+++ b/ui/src/assets/icons/file-types/html.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/http.svg b/ui/src/assets/icons/file-types/http.svg
new file mode 100644
index 0000000..94574d4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/http.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/huff.svg b/ui/src/assets/icons/file-types/huff.svg
new file mode 100644
index 0000000..2232914
--- /dev/null
+++ b/ui/src/assets/icons/file-types/huff.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/huff_light.svg b/ui/src/assets/icons/file-types/huff_light.svg
new file mode 100644
index 0000000..43889e0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/huff_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/hurl.svg b/ui/src/assets/icons/file-types/hurl.svg
new file mode 100644
index 0000000..227045b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/hurl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/husky.svg b/ui/src/assets/icons/file-types/husky.svg
new file mode 100644
index 0000000..b48f06a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/husky.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/i18n.svg b/ui/src/assets/icons/file-types/i18n.svg
new file mode 100644
index 0000000..4f678de
--- /dev/null
+++ b/ui/src/assets/icons/file-types/i18n.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/idris.svg b/ui/src/assets/icons/file-types/idris.svg
new file mode 100644
index 0000000..445745b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/idris.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ifanr-cloud.svg b/ui/src/assets/icons/file-types/ifanr-cloud.svg
new file mode 100644
index 0000000..c356b16
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ifanr-cloud.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/image.svg b/ui/src/assets/icons/file-types/image.svg
new file mode 100644
index 0000000..0ca446b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/image.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/imba.svg b/ui/src/assets/icons/file-types/imba.svg
new file mode 100644
index 0000000..60b0615
--- /dev/null
+++ b/ui/src/assets/icons/file-types/imba.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/installation.svg b/ui/src/assets/icons/file-types/installation.svg
new file mode 100644
index 0000000..36fa21c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/installation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ionic.svg b/ui/src/assets/icons/file-types/ionic.svg
new file mode 100644
index 0000000..2ce630d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ionic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/istanbul.svg b/ui/src/assets/icons/file-types/istanbul.svg
new file mode 100644
index 0000000..9508a98
--- /dev/null
+++ b/ui/src/assets/icons/file-types/istanbul.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/jar.svg b/ui/src/assets/icons/file-types/jar.svg
new file mode 100644
index 0000000..1c81c48
--- /dev/null
+++ b/ui/src/assets/icons/file-types/jar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/java.svg b/ui/src/assets/icons/file-types/java.svg
new file mode 100644
index 0000000..0950bc4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/java.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/javaclass.svg b/ui/src/assets/icons/file-types/javaclass.svg
new file mode 100644
index 0000000..9abe7ad
--- /dev/null
+++ b/ui/src/assets/icons/file-types/javaclass.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/javascript-map.svg b/ui/src/assets/icons/file-types/javascript-map.svg
new file mode 100644
index 0000000..a1fcc22
--- /dev/null
+++ b/ui/src/assets/icons/file-types/javascript-map.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/javascript.svg b/ui/src/assets/icons/file-types/javascript.svg
new file mode 100644
index 0000000..254704a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/javascript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/jenkins.svg b/ui/src/assets/icons/file-types/jenkins.svg
new file mode 100644
index 0000000..1517b74
--- /dev/null
+++ b/ui/src/assets/icons/file-types/jenkins.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/jest.svg b/ui/src/assets/icons/file-types/jest.svg
new file mode 100644
index 0000000..fb40ecb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/jest.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/jinja.svg b/ui/src/assets/icons/file-types/jinja.svg
new file mode 100644
index 0000000..8163f2a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/jinja.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/jinja_light.svg b/ui/src/assets/icons/file-types/jinja_light.svg
new file mode 100644
index 0000000..2233398
--- /dev/null
+++ b/ui/src/assets/icons/file-types/jinja_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/jsconfig.svg b/ui/src/assets/icons/file-types/jsconfig.svg
new file mode 100644
index 0000000..5aef481
--- /dev/null
+++ b/ui/src/assets/icons/file-types/jsconfig.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/json.svg b/ui/src/assets/icons/file-types/json.svg
new file mode 100644
index 0000000..2590b94
--- /dev/null
+++ b/ui/src/assets/icons/file-types/json.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/jsr.svg b/ui/src/assets/icons/file-types/jsr.svg
new file mode 100644
index 0000000..739f657
--- /dev/null
+++ b/ui/src/assets/icons/file-types/jsr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/jsr_light.svg b/ui/src/assets/icons/file-types/jsr_light.svg
new file mode 100644
index 0000000..c93d452
--- /dev/null
+++ b/ui/src/assets/icons/file-types/jsr_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/julia.svg b/ui/src/assets/icons/file-types/julia.svg
new file mode 100644
index 0000000..39fca63
--- /dev/null
+++ b/ui/src/assets/icons/file-types/julia.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/jupyter.svg b/ui/src/assets/icons/file-types/jupyter.svg
new file mode 100644
index 0000000..770bffb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/jupyter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/just.svg b/ui/src/assets/icons/file-types/just.svg
new file mode 100644
index 0000000..7fc7543
--- /dev/null
+++ b/ui/src/assets/icons/file-types/just.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/karma.svg b/ui/src/assets/icons/file-types/karma.svg
new file mode 100644
index 0000000..0db4ab6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/karma.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/kcl.svg b/ui/src/assets/icons/file-types/kcl.svg
new file mode 100644
index 0000000..4f10c60
--- /dev/null
+++ b/ui/src/assets/icons/file-types/kcl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/key.svg b/ui/src/assets/icons/file-types/key.svg
new file mode 100644
index 0000000..08f67af
--- /dev/null
+++ b/ui/src/assets/icons/file-types/key.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/keystatic.svg b/ui/src/assets/icons/file-types/keystatic.svg
new file mode 100644
index 0000000..087b658
--- /dev/null
+++ b/ui/src/assets/icons/file-types/keystatic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/kivy.svg b/ui/src/assets/icons/file-types/kivy.svg
new file mode 100644
index 0000000..2a1a35c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/kivy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/kl.svg b/ui/src/assets/icons/file-types/kl.svg
new file mode 100644
index 0000000..967ef09
--- /dev/null
+++ b/ui/src/assets/icons/file-types/kl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/knip.svg b/ui/src/assets/icons/file-types/knip.svg
new file mode 100644
index 0000000..c71d0a2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/knip.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/kotlin.svg b/ui/src/assets/icons/file-types/kotlin.svg
new file mode 100644
index 0000000..740505c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/kotlin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/kubernetes.svg b/ui/src/assets/icons/file-types/kubernetes.svg
new file mode 100644
index 0000000..6726dcc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/kubernetes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/kusto.svg b/ui/src/assets/icons/file-types/kusto.svg
new file mode 100644
index 0000000..46087e8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/kusto.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/label.svg b/ui/src/assets/icons/file-types/label.svg
new file mode 100644
index 0000000..28abeac
--- /dev/null
+++ b/ui/src/assets/icons/file-types/label.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/laravel.svg b/ui/src/assets/icons/file-types/laravel.svg
new file mode 100644
index 0000000..95ee923
--- /dev/null
+++ b/ui/src/assets/icons/file-types/laravel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/latexmk.svg b/ui/src/assets/icons/file-types/latexmk.svg
new file mode 100644
index 0000000..484318a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/latexmk.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lbx.svg b/ui/src/assets/icons/file-types/lbx.svg
new file mode 100644
index 0000000..c66f157
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lbx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lefthook.svg b/ui/src/assets/icons/file-types/lefthook.svg
new file mode 100644
index 0000000..93f6f81
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lefthook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lerna.svg b/ui/src/assets/icons/file-types/lerna.svg
new file mode 100644
index 0000000..4128d6b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lerna.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/less.svg b/ui/src/assets/icons/file-types/less.svg
new file mode 100644
index 0000000..2e13a3c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/less.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/liara.svg b/ui/src/assets/icons/file-types/liara.svg
new file mode 100644
index 0000000..2fd408c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/liara.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lib.svg b/ui/src/assets/icons/file-types/lib.svg
new file mode 100644
index 0000000..7c8fda3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lib.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lighthouse.svg b/ui/src/assets/icons/file-types/lighthouse.svg
new file mode 100644
index 0000000..0229244
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lighthouse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lilypond.svg b/ui/src/assets/icons/file-types/lilypond.svg
new file mode 100644
index 0000000..a12aa2c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lilypond.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lintstaged.svg b/ui/src/assets/icons/file-types/lintstaged.svg
new file mode 100644
index 0000000..fbf9467
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lintstaged.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/liquid.svg b/ui/src/assets/icons/file-types/liquid.svg
new file mode 100644
index 0000000..5111ab6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/liquid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lisp.svg b/ui/src/assets/icons/file-types/lisp.svg
new file mode 100644
index 0000000..76e4f46
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lisp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/livescript.svg b/ui/src/assets/icons/file-types/livescript.svg
new file mode 100644
index 0000000..d7dcb37
--- /dev/null
+++ b/ui/src/assets/icons/file-types/livescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lock.svg b/ui/src/assets/icons/file-types/lock.svg
new file mode 100644
index 0000000..ca49d02
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/log.svg b/ui/src/assets/icons/file-types/log.svg
new file mode 100644
index 0000000..389f51d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/log.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lolcode.svg b/ui/src/assets/icons/file-types/lolcode.svg
new file mode 100644
index 0000000..f9c7595
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lolcode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lottie.svg b/ui/src/assets/icons/file-types/lottie.svg
new file mode 100644
index 0000000..29981d3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lottie.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lua.svg b/ui/src/assets/icons/file-types/lua.svg
new file mode 100644
index 0000000..ca7a3d5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lua.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/luau.svg b/ui/src/assets/icons/file-types/luau.svg
new file mode 100644
index 0000000..7f9ad57
--- /dev/null
+++ b/ui/src/assets/icons/file-types/luau.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/lyric.svg b/ui/src/assets/icons/file-types/lyric.svg
new file mode 100644
index 0000000..06bb43e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/lyric.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/makefile.svg b/ui/src/assets/icons/file-types/makefile.svg
new file mode 100644
index 0000000..e211671
--- /dev/null
+++ b/ui/src/assets/icons/file-types/makefile.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/markdoc-config.svg b/ui/src/assets/icons/file-types/markdoc-config.svg
new file mode 100644
index 0000000..13913c3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/markdoc-config.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/markdoc.svg b/ui/src/assets/icons/file-types/markdoc.svg
new file mode 100644
index 0000000..3ed2c54
--- /dev/null
+++ b/ui/src/assets/icons/file-types/markdoc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/markdown.svg b/ui/src/assets/icons/file-types/markdown.svg
new file mode 100644
index 0000000..4c22434
--- /dev/null
+++ b/ui/src/assets/icons/file-types/markdown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/markdownlint.svg b/ui/src/assets/icons/file-types/markdownlint.svg
new file mode 100644
index 0000000..37daf0d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/markdownlint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/markojs.svg b/ui/src/assets/icons/file-types/markojs.svg
new file mode 100644
index 0000000..938b6fe
--- /dev/null
+++ b/ui/src/assets/icons/file-types/markojs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mathematica.svg b/ui/src/assets/icons/file-types/mathematica.svg
new file mode 100644
index 0000000..08c2508
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mathematica.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/matlab.svg b/ui/src/assets/icons/file-types/matlab.svg
new file mode 100644
index 0000000..a2166f8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/matlab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/maven.svg b/ui/src/assets/icons/file-types/maven.svg
new file mode 100644
index 0000000..7a88745
--- /dev/null
+++ b/ui/src/assets/icons/file-types/maven.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mdsvex.svg b/ui/src/assets/icons/file-types/mdsvex.svg
new file mode 100644
index 0000000..34b252a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mdsvex.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mdx.svg b/ui/src/assets/icons/file-types/mdx.svg
new file mode 100644
index 0000000..b2ab561
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mdx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mercurial.svg b/ui/src/assets/icons/file-types/mercurial.svg
new file mode 100644
index 0000000..41f701e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mercurial.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/merlin.svg b/ui/src/assets/icons/file-types/merlin.svg
new file mode 100644
index 0000000..96b29d3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/merlin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mermaid.svg b/ui/src/assets/icons/file-types/mermaid.svg
new file mode 100644
index 0000000..b1f520d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mermaid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/meson.svg b/ui/src/assets/icons/file-types/meson.svg
new file mode 100644
index 0000000..f9d3bef
--- /dev/null
+++ b/ui/src/assets/icons/file-types/meson.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/minecraft-fabric.svg b/ui/src/assets/icons/file-types/minecraft-fabric.svg
new file mode 100644
index 0000000..4c0985b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/minecraft-fabric.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/minecraft.svg b/ui/src/assets/icons/file-types/minecraft.svg
new file mode 100644
index 0000000..219af8a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/minecraft.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mint.svg b/ui/src/assets/icons/file-types/mint.svg
new file mode 100644
index 0000000..659340a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mjml.svg b/ui/src/assets/icons/file-types/mjml.svg
new file mode 100644
index 0000000..5580ca0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mjml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mocha.svg b/ui/src/assets/icons/file-types/mocha.svg
new file mode 100644
index 0000000..bce8ac3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mocha.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/modernizr.svg b/ui/src/assets/icons/file-types/modernizr.svg
new file mode 100644
index 0000000..b340bec
--- /dev/null
+++ b/ui/src/assets/icons/file-types/modernizr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mojo.svg b/ui/src/assets/icons/file-types/mojo.svg
new file mode 100644
index 0000000..505a8f5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mojo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/moon.svg b/ui/src/assets/icons/file-types/moon.svg
new file mode 100644
index 0000000..c428ebb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/moon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/moonscript.svg b/ui/src/assets/icons/file-types/moonscript.svg
new file mode 100644
index 0000000..1d7f7ee
--- /dev/null
+++ b/ui/src/assets/icons/file-types/moonscript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/mxml.svg b/ui/src/assets/icons/file-types/mxml.svg
new file mode 100644
index 0000000..c5b84dd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/mxml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nano-staged.svg b/ui/src/assets/icons/file-types/nano-staged.svg
new file mode 100644
index 0000000..6e6cd07
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nano-staged.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nano-staged_light.svg b/ui/src/assets/icons/file-types/nano-staged_light.svg
new file mode 100644
index 0000000..698232f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nano-staged_light.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/ui/src/assets/icons/file-types/ndst.svg b/ui/src/assets/icons/file-types/ndst.svg
new file mode 100644
index 0000000..1941313
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ndst.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nest.svg b/ui/src/assets/icons/file-types/nest.svg
new file mode 100644
index 0000000..259dc53
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nest.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/netlify.svg b/ui/src/assets/icons/file-types/netlify.svg
new file mode 100644
index 0000000..27c837f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/netlify.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/netlify_light.svg b/ui/src/assets/icons/file-types/netlify_light.svg
new file mode 100644
index 0000000..b142c48
--- /dev/null
+++ b/ui/src/assets/icons/file-types/netlify_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/next.svg b/ui/src/assets/icons/file-types/next.svg
new file mode 100644
index 0000000..83fee37
--- /dev/null
+++ b/ui/src/assets/icons/file-types/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/next_light.svg b/ui/src/assets/icons/file-types/next_light.svg
new file mode 100644
index 0000000..6e5fb27
--- /dev/null
+++ b/ui/src/assets/icons/file-types/next_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nginx.svg b/ui/src/assets/icons/file-types/nginx.svg
new file mode 100644
index 0000000..658ad22
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nginx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ngrx-actions.svg b/ui/src/assets/icons/file-types/ngrx-actions.svg
new file mode 100644
index 0000000..de418d4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ngrx-actions.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ngrx-effects.svg b/ui/src/assets/icons/file-types/ngrx-effects.svg
new file mode 100644
index 0000000..8f7dc89
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ngrx-effects.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ngrx-entity.svg b/ui/src/assets/icons/file-types/ngrx-entity.svg
new file mode 100644
index 0000000..af0dd05
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ngrx-entity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ngrx-reducer.svg b/ui/src/assets/icons/file-types/ngrx-reducer.svg
new file mode 100644
index 0000000..db7a553
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ngrx-reducer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ngrx-selectors.svg b/ui/src/assets/icons/file-types/ngrx-selectors.svg
new file mode 100644
index 0000000..af03c40
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ngrx-selectors.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ngrx-state.svg b/ui/src/assets/icons/file-types/ngrx-state.svg
new file mode 100644
index 0000000..258c0ac
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ngrx-state.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nim.svg b/ui/src/assets/icons/file-types/nim.svg
new file mode 100644
index 0000000..d985bb4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nim.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nix.svg b/ui/src/assets/icons/file-types/nix.svg
new file mode 100644
index 0000000..a507609
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nix.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nodejs.svg b/ui/src/assets/icons/file-types/nodejs.svg
new file mode 100644
index 0000000..ba73901
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nodejs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nodejs_alt.svg b/ui/src/assets/icons/file-types/nodejs_alt.svg
new file mode 100644
index 0000000..5b70be2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nodejs_alt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nodemon.svg b/ui/src/assets/icons/file-types/nodemon.svg
new file mode 100644
index 0000000..2bd35d1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nodemon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/npm.svg b/ui/src/assets/icons/file-types/npm.svg
new file mode 100644
index 0000000..87aa583
--- /dev/null
+++ b/ui/src/assets/icons/file-types/npm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nuget.svg b/ui/src/assets/icons/file-types/nuget.svg
new file mode 100644
index 0000000..82e298f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nuget.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nunjucks.svg b/ui/src/assets/icons/file-types/nunjucks.svg
new file mode 100644
index 0000000..9fb8890
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nunjucks.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nuxt.svg b/ui/src/assets/icons/file-types/nuxt.svg
new file mode 100644
index 0000000..babf919
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nuxt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/nx.svg b/ui/src/assets/icons/file-types/nx.svg
new file mode 100644
index 0000000..8db8323
--- /dev/null
+++ b/ui/src/assets/icons/file-types/nx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/objective-c.svg b/ui/src/assets/icons/file-types/objective-c.svg
new file mode 100644
index 0000000..7a69f91
--- /dev/null
+++ b/ui/src/assets/icons/file-types/objective-c.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/objective-cpp.svg b/ui/src/assets/icons/file-types/objective-cpp.svg
new file mode 100644
index 0000000..cd55d1e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/objective-cpp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ocaml.svg b/ui/src/assets/icons/file-types/ocaml.svg
new file mode 100644
index 0000000..cb6eb6b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ocaml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/odin.svg b/ui/src/assets/icons/file-types/odin.svg
new file mode 100644
index 0000000..1877a6c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/odin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/opa.svg b/ui/src/assets/icons/file-types/opa.svg
new file mode 100644
index 0000000..3afc1c6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/opa.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/opam.svg b/ui/src/assets/icons/file-types/opam.svg
new file mode 100644
index 0000000..70f1b7f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/opam.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/openapi.svg b/ui/src/assets/icons/file-types/openapi.svg
new file mode 100644
index 0000000..5b367a1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/openapi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/openapi_light.svg b/ui/src/assets/icons/file-types/openapi_light.svg
new file mode 100644
index 0000000..179006d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/openapi_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/otne.svg b/ui/src/assets/icons/file-types/otne.svg
new file mode 100644
index 0000000..8670a61
--- /dev/null
+++ b/ui/src/assets/icons/file-types/otne.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/oxlint.svg b/ui/src/assets/icons/file-types/oxlint.svg
new file mode 100644
index 0000000..2ffad92
--- /dev/null
+++ b/ui/src/assets/icons/file-types/oxlint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/packship.svg b/ui/src/assets/icons/file-types/packship.svg
new file mode 100644
index 0000000..e03b35d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/packship.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/palette.svg b/ui/src/assets/icons/file-types/palette.svg
new file mode 100644
index 0000000..cc27f66
--- /dev/null
+++ b/ui/src/assets/icons/file-types/palette.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/panda.svg b/ui/src/assets/icons/file-types/panda.svg
new file mode 100644
index 0000000..dde4122
--- /dev/null
+++ b/ui/src/assets/icons/file-types/panda.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/parcel.svg b/ui/src/assets/icons/file-types/parcel.svg
new file mode 100644
index 0000000..39a1835
--- /dev/null
+++ b/ui/src/assets/icons/file-types/parcel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pascal.svg b/ui/src/assets/icons/file-types/pascal.svg
new file mode 100644
index 0000000..b0a2993
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pascal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pawn.svg b/ui/src/assets/icons/file-types/pawn.svg
new file mode 100644
index 0000000..b615d75
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pawn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/payload.svg b/ui/src/assets/icons/file-types/payload.svg
new file mode 100644
index 0000000..8e1e82a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/payload.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/payload_light.svg b/ui/src/assets/icons/file-types/payload_light.svg
new file mode 100644
index 0000000..7a4e9c7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/payload_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pdf.svg b/ui/src/assets/icons/file-types/pdf.svg
new file mode 100644
index 0000000..1c84fe8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pdf.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pdm.svg b/ui/src/assets/icons/file-types/pdm.svg
new file mode 100644
index 0000000..dd23bb3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pdm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/percy.svg b/ui/src/assets/icons/file-types/percy.svg
new file mode 100644
index 0000000..6d0f897
--- /dev/null
+++ b/ui/src/assets/icons/file-types/percy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/perl.svg b/ui/src/assets/icons/file-types/perl.svg
new file mode 100644
index 0000000..0534cad
--- /dev/null
+++ b/ui/src/assets/icons/file-types/perl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/php-cs-fixer.svg b/ui/src/assets/icons/file-types/php-cs-fixer.svg
new file mode 100644
index 0000000..398c214
--- /dev/null
+++ b/ui/src/assets/icons/file-types/php-cs-fixer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/php.svg b/ui/src/assets/icons/file-types/php.svg
new file mode 100644
index 0000000..1d7e336
--- /dev/null
+++ b/ui/src/assets/icons/file-types/php.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/php_elephant.svg b/ui/src/assets/icons/file-types/php_elephant.svg
new file mode 100644
index 0000000..d2c2995
--- /dev/null
+++ b/ui/src/assets/icons/file-types/php_elephant.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/php_elephant_pink.svg b/ui/src/assets/icons/file-types/php_elephant_pink.svg
new file mode 100644
index 0000000..a7cad74
--- /dev/null
+++ b/ui/src/assets/icons/file-types/php_elephant_pink.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/phpstan.svg b/ui/src/assets/icons/file-types/phpstan.svg
new file mode 100644
index 0000000..34b612f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/phpstan.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/phpunit.svg b/ui/src/assets/icons/file-types/phpunit.svg
new file mode 100644
index 0000000..2132200
--- /dev/null
+++ b/ui/src/assets/icons/file-types/phpunit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pinejs.svg b/ui/src/assets/icons/file-types/pinejs.svg
new file mode 100644
index 0000000..44c0020
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pinejs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pipeline.svg b/ui/src/assets/icons/file-types/pipeline.svg
new file mode 100644
index 0000000..a3a5e66
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pipeline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pkl.svg b/ui/src/assets/icons/file-types/pkl.svg
new file mode 100644
index 0000000..3f31ead
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pkl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/plastic.svg b/ui/src/assets/icons/file-types/plastic.svg
new file mode 100644
index 0000000..cc00e5a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/plastic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/playwright.svg b/ui/src/assets/icons/file-types/playwright.svg
new file mode 100644
index 0000000..cae0b24
--- /dev/null
+++ b/ui/src/assets/icons/file-types/playwright.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/plop.svg b/ui/src/assets/icons/file-types/plop.svg
new file mode 100644
index 0000000..85e3bd2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/plop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pm2-ecosystem.svg b/ui/src/assets/icons/file-types/pm2-ecosystem.svg
new file mode 100644
index 0000000..a99d5f2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pm2-ecosystem.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pnpm.svg b/ui/src/assets/icons/file-types/pnpm.svg
new file mode 100644
index 0000000..fc52c6e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pnpm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pnpm_light.svg b/ui/src/assets/icons/file-types/pnpm_light.svg
new file mode 100644
index 0000000..4236956
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pnpm_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/poetry.svg b/ui/src/assets/icons/file-types/poetry.svg
new file mode 100644
index 0000000..4a355a7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/poetry.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/postcss.svg b/ui/src/assets/icons/file-types/postcss.svg
new file mode 100644
index 0000000..799edeb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/postcss.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/posthtml.svg b/ui/src/assets/icons/file-types/posthtml.svg
new file mode 100644
index 0000000..54dda3c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/posthtml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/powerpoint.svg b/ui/src/assets/icons/file-types/powerpoint.svg
new file mode 100644
index 0000000..eaba916
--- /dev/null
+++ b/ui/src/assets/icons/file-types/powerpoint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/powershell.svg b/ui/src/assets/icons/file-types/powershell.svg
new file mode 100644
index 0000000..a266393
--- /dev/null
+++ b/ui/src/assets/icons/file-types/powershell.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pre-commit.svg b/ui/src/assets/icons/file-types/pre-commit.svg
new file mode 100644
index 0000000..399826b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pre-commit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/prettier.svg b/ui/src/assets/icons/file-types/prettier.svg
new file mode 100644
index 0000000..a6cda34
--- /dev/null
+++ b/ui/src/assets/icons/file-types/prettier.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/prisma.svg b/ui/src/assets/icons/file-types/prisma.svg
new file mode 100644
index 0000000..121abea
--- /dev/null
+++ b/ui/src/assets/icons/file-types/prisma.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/processing.svg b/ui/src/assets/icons/file-types/processing.svg
new file mode 100644
index 0000000..8a960ab
--- /dev/null
+++ b/ui/src/assets/icons/file-types/processing.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/prolog.svg b/ui/src/assets/icons/file-types/prolog.svg
new file mode 100644
index 0000000..7eda090
--- /dev/null
+++ b/ui/src/assets/icons/file-types/prolog.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/prompt.svg b/ui/src/assets/icons/file-types/prompt.svg
new file mode 100644
index 0000000..aa37366
--- /dev/null
+++ b/ui/src/assets/icons/file-types/prompt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/proto.svg b/ui/src/assets/icons/file-types/proto.svg
new file mode 100644
index 0000000..7757c0e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/proto.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/protractor.svg b/ui/src/assets/icons/file-types/protractor.svg
new file mode 100644
index 0000000..50f4643
--- /dev/null
+++ b/ui/src/assets/icons/file-types/protractor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pug.svg b/ui/src/assets/icons/file-types/pug.svg
new file mode 100644
index 0000000..62a3602
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pug.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/puppet.svg b/ui/src/assets/icons/file-types/puppet.svg
new file mode 100644
index 0000000..3e1e9c1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/puppet.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/puppeteer.svg b/ui/src/assets/icons/file-types/puppeteer.svg
new file mode 100644
index 0000000..b553df3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/puppeteer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/purescript.svg b/ui/src/assets/icons/file-types/purescript.svg
new file mode 100644
index 0000000..d82c8f9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/purescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/python-misc.svg b/ui/src/assets/icons/file-types/python-misc.svg
new file mode 100644
index 0000000..44fb730
--- /dev/null
+++ b/ui/src/assets/icons/file-types/python-misc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/python.svg b/ui/src/assets/icons/file-types/python.svg
new file mode 100644
index 0000000..20c2508
--- /dev/null
+++ b/ui/src/assets/icons/file-types/python.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/pytorch.svg b/ui/src/assets/icons/file-types/pytorch.svg
new file mode 100644
index 0000000..4cb85d0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/pytorch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/qsharp.svg b/ui/src/assets/icons/file-types/qsharp.svg
new file mode 100644
index 0000000..de9838d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/qsharp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/quarto.svg b/ui/src/assets/icons/file-types/quarto.svg
new file mode 100644
index 0000000..3bb8ef7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/quarto.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/quasar.svg b/ui/src/assets/icons/file-types/quasar.svg
new file mode 100644
index 0000000..fa02ff0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/quasar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/quokka.svg b/ui/src/assets/icons/file-types/quokka.svg
new file mode 100644
index 0000000..bf368de
--- /dev/null
+++ b/ui/src/assets/icons/file-types/quokka.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/qwik.svg b/ui/src/assets/icons/file-types/qwik.svg
new file mode 100644
index 0000000..5555116
--- /dev/null
+++ b/ui/src/assets/icons/file-types/qwik.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/r.svg b/ui/src/assets/icons/file-types/r.svg
new file mode 100644
index 0000000..5703dd0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/r.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/racket.svg b/ui/src/assets/icons/file-types/racket.svg
new file mode 100644
index 0000000..04ca144
--- /dev/null
+++ b/ui/src/assets/icons/file-types/racket.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/raml.svg b/ui/src/assets/icons/file-types/raml.svg
new file mode 100644
index 0000000..d35d561
--- /dev/null
+++ b/ui/src/assets/icons/file-types/raml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/razor.svg b/ui/src/assets/icons/file-types/razor.svg
new file mode 100644
index 0000000..4e99091
--- /dev/null
+++ b/ui/src/assets/icons/file-types/razor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rbxmk.svg b/ui/src/assets/icons/file-types/rbxmk.svg
new file mode 100644
index 0000000..e7d4953
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rbxmk.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rc.svg b/ui/src/assets/icons/file-types/rc.svg
new file mode 100644
index 0000000..83040db
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/react.svg b/ui/src/assets/icons/file-types/react.svg
new file mode 100644
index 0000000..ced90db
--- /dev/null
+++ b/ui/src/assets/icons/file-types/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/react_ts.svg b/ui/src/assets/icons/file-types/react_ts.svg
new file mode 100644
index 0000000..887f72c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/react_ts.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/readme.svg b/ui/src/assets/icons/file-types/readme.svg
new file mode 100644
index 0000000..943d08f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/readme.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/reason.svg b/ui/src/assets/icons/file-types/reason.svg
new file mode 100644
index 0000000..0f4b3e1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/reason.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/red.svg b/ui/src/assets/icons/file-types/red.svg
new file mode 100644
index 0000000..6084231
--- /dev/null
+++ b/ui/src/assets/icons/file-types/red.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/redux-action.svg b/ui/src/assets/icons/file-types/redux-action.svg
new file mode 100644
index 0000000..a4872e7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/redux-action.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/redux-reducer.svg b/ui/src/assets/icons/file-types/redux-reducer.svg
new file mode 100644
index 0000000..cfcca98
--- /dev/null
+++ b/ui/src/assets/icons/file-types/redux-reducer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/redux-selector.svg b/ui/src/assets/icons/file-types/redux-selector.svg
new file mode 100644
index 0000000..073c286
--- /dev/null
+++ b/ui/src/assets/icons/file-types/redux-selector.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/redux-store.svg b/ui/src/assets/icons/file-types/redux-store.svg
new file mode 100644
index 0000000..8e644e7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/redux-store.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/regedit.svg b/ui/src/assets/icons/file-types/regedit.svg
new file mode 100644
index 0000000..3d63206
--- /dev/null
+++ b/ui/src/assets/icons/file-types/regedit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/remark.svg b/ui/src/assets/icons/file-types/remark.svg
new file mode 100644
index 0000000..9d6a918
--- /dev/null
+++ b/ui/src/assets/icons/file-types/remark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/remix.svg b/ui/src/assets/icons/file-types/remix.svg
new file mode 100644
index 0000000..763f57f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/remix.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/remix_light.svg b/ui/src/assets/icons/file-types/remix_light.svg
new file mode 100644
index 0000000..748b779
--- /dev/null
+++ b/ui/src/assets/icons/file-types/remix_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/renovate.svg b/ui/src/assets/icons/file-types/renovate.svg
new file mode 100644
index 0000000..bc63cbb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/renovate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/replit.svg b/ui/src/assets/icons/file-types/replit.svg
new file mode 100644
index 0000000..f1478a5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/replit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rescript-interface.svg b/ui/src/assets/icons/file-types/rescript-interface.svg
new file mode 100644
index 0000000..db30553
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rescript-interface.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rescript.svg b/ui/src/assets/icons/file-types/rescript.svg
new file mode 100644
index 0000000..8f40a3a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/restql.svg b/ui/src/assets/icons/file-types/restql.svg
new file mode 100644
index 0000000..a056fe9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/restql.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/riot.svg b/ui/src/assets/icons/file-types/riot.svg
new file mode 100644
index 0000000..587e50d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/riot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/roadmap.svg b/ui/src/assets/icons/file-types/roadmap.svg
new file mode 100644
index 0000000..2279ead
--- /dev/null
+++ b/ui/src/assets/icons/file-types/roadmap.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/roblox.svg b/ui/src/assets/icons/file-types/roblox.svg
new file mode 100644
index 0000000..56cc378
--- /dev/null
+++ b/ui/src/assets/icons/file-types/roblox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/robot.svg b/ui/src/assets/icons/file-types/robot.svg
new file mode 100644
index 0000000..36c7225
--- /dev/null
+++ b/ui/src/assets/icons/file-types/robot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/robots.svg b/ui/src/assets/icons/file-types/robots.svg
new file mode 100644
index 0000000..11fdaae
--- /dev/null
+++ b/ui/src/assets/icons/file-types/robots.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rocket.svg b/ui/src/assets/icons/file-types/rocket.svg
new file mode 100644
index 0000000..5f62f32
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rocket.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rojo.svg b/ui/src/assets/icons/file-types/rojo.svg
new file mode 100644
index 0000000..37c46ea
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rojo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rollup.svg b/ui/src/assets/icons/file-types/rollup.svg
new file mode 100644
index 0000000..7fa0153
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rollup.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rome.svg b/ui/src/assets/icons/file-types/rome.svg
new file mode 100644
index 0000000..8f5de92
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rome.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/routing.svg b/ui/src/assets/icons/file-types/routing.svg
new file mode 100644
index 0000000..ea02c90
--- /dev/null
+++ b/ui/src/assets/icons/file-types/routing.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rspec.svg b/ui/src/assets/icons/file-types/rspec.svg
new file mode 100644
index 0000000..c1bf424
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rspec.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rubocop.svg b/ui/src/assets/icons/file-types/rubocop.svg
new file mode 100644
index 0000000..e6a24a2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rubocop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rubocop_light.svg b/ui/src/assets/icons/file-types/rubocop_light.svg
new file mode 100644
index 0000000..689c023
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rubocop_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ruby.svg b/ui/src/assets/icons/file-types/ruby.svg
new file mode 100644
index 0000000..2e3215d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ruby.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/ruff.svg b/ui/src/assets/icons/file-types/ruff.svg
new file mode 100644
index 0000000..a526788
--- /dev/null
+++ b/ui/src/assets/icons/file-types/ruff.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/rust.svg b/ui/src/assets/icons/file-types/rust.svg
new file mode 100644
index 0000000..b382aa4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/rust.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/salesforce.svg b/ui/src/assets/icons/file-types/salesforce.svg
new file mode 100644
index 0000000..80e1aa9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/salesforce.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/san.svg b/ui/src/assets/icons/file-types/san.svg
new file mode 100644
index 0000000..d17b9fa
--- /dev/null
+++ b/ui/src/assets/icons/file-types/san.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sas.svg b/ui/src/assets/icons/file-types/sas.svg
new file mode 100644
index 0000000..d47c8bd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sas.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sass.svg b/ui/src/assets/icons/file-types/sass.svg
new file mode 100644
index 0000000..6f39acb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sass.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sbt.svg b/ui/src/assets/icons/file-types/sbt.svg
new file mode 100644
index 0000000..37587c5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sbt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/scala.svg b/ui/src/assets/icons/file-types/scala.svg
new file mode 100644
index 0000000..08e0c2d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/scala.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/scheme.svg b/ui/src/assets/icons/file-types/scheme.svg
new file mode 100644
index 0000000..c8f986e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/scheme.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/scons.svg b/ui/src/assets/icons/file-types/scons.svg
new file mode 100644
index 0000000..d584ea8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/scons.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/scons_light.svg b/ui/src/assets/icons/file-types/scons_light.svg
new file mode 100644
index 0000000..31f88d5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/scons_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/screwdriver.svg b/ui/src/assets/icons/file-types/screwdriver.svg
new file mode 100644
index 0000000..cac8206
--- /dev/null
+++ b/ui/src/assets/icons/file-types/screwdriver.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/search.svg b/ui/src/assets/icons/file-types/search.svg
new file mode 100644
index 0000000..3d35c8e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/semantic-release.svg b/ui/src/assets/icons/file-types/semantic-release.svg
new file mode 100644
index 0000000..17187e8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/semantic-release.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/semantic-release_light.svg b/ui/src/assets/icons/file-types/semantic-release_light.svg
new file mode 100644
index 0000000..21e42a0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/semantic-release_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/semgrep.svg b/ui/src/assets/icons/file-types/semgrep.svg
new file mode 100644
index 0000000..73a8abc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/semgrep.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sentry.svg b/ui/src/assets/icons/file-types/sentry.svg
new file mode 100644
index 0000000..319e60a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sentry.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sequelize.svg b/ui/src/assets/icons/file-types/sequelize.svg
new file mode 100644
index 0000000..0e4c788
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sequelize.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/serverless.svg b/ui/src/assets/icons/file-types/serverless.svg
new file mode 100644
index 0000000..92ccca8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/serverless.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/settings.svg b/ui/src/assets/icons/file-types/settings.svg
new file mode 100644
index 0000000..dc701ae
--- /dev/null
+++ b/ui/src/assets/icons/file-types/settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/shader.svg b/ui/src/assets/icons/file-types/shader.svg
new file mode 100644
index 0000000..f42156e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/shader.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/silverstripe.svg b/ui/src/assets/icons/file-types/silverstripe.svg
new file mode 100644
index 0000000..46cdb7e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/silverstripe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/simulink.svg b/ui/src/assets/icons/file-types/simulink.svg
new file mode 100644
index 0000000..33e97fe
--- /dev/null
+++ b/ui/src/assets/icons/file-types/simulink.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/siyuan.svg b/ui/src/assets/icons/file-types/siyuan.svg
new file mode 100644
index 0000000..7a7488d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/siyuan.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sketch.svg b/ui/src/assets/icons/file-types/sketch.svg
new file mode 100644
index 0000000..0d75406
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sketch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/slim.svg b/ui/src/assets/icons/file-types/slim.svg
new file mode 100644
index 0000000..edc7241
--- /dev/null
+++ b/ui/src/assets/icons/file-types/slim.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/slint.svg b/ui/src/assets/icons/file-types/slint.svg
new file mode 100644
index 0000000..b6434ec
--- /dev/null
+++ b/ui/src/assets/icons/file-types/slint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/slug.svg b/ui/src/assets/icons/file-types/slug.svg
new file mode 100644
index 0000000..da1dcc7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/slug.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/smarty.svg b/ui/src/assets/icons/file-types/smarty.svg
new file mode 100644
index 0000000..4572a58
--- /dev/null
+++ b/ui/src/assets/icons/file-types/smarty.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sml.svg b/ui/src/assets/icons/file-types/sml.svg
new file mode 100644
index 0000000..8f92a33
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/snakemake.svg b/ui/src/assets/icons/file-types/snakemake.svg
new file mode 100644
index 0000000..6dd08c9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/snakemake.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/snapcraft.svg b/ui/src/assets/icons/file-types/snapcraft.svg
new file mode 100644
index 0000000..17bf8d8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/snapcraft.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/snowpack.svg b/ui/src/assets/icons/file-types/snowpack.svg
new file mode 100644
index 0000000..7941fae
--- /dev/null
+++ b/ui/src/assets/icons/file-types/snowpack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/snowpack_light.svg b/ui/src/assets/icons/file-types/snowpack_light.svg
new file mode 100644
index 0000000..70389d2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/snowpack_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/snyk.svg b/ui/src/assets/icons/file-types/snyk.svg
new file mode 100644
index 0000000..90791ee
--- /dev/null
+++ b/ui/src/assets/icons/file-types/snyk.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/solidity.svg b/ui/src/assets/icons/file-types/solidity.svg
new file mode 100644
index 0000000..6ae9873
--- /dev/null
+++ b/ui/src/assets/icons/file-types/solidity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sonarcloud.svg b/ui/src/assets/icons/file-types/sonarcloud.svg
new file mode 100644
index 0000000..ee98961
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sonarcloud.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sprite.svg b/ui/src/assets/icons/file-types/sprite.svg
new file mode 100644
index 0000000..c46adad
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sprite.svg
@@ -0,0 +1,6509 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/spwn.svg b/ui/src/assets/icons/file-types/spwn.svg
new file mode 100644
index 0000000..8a8bf43
--- /dev/null
+++ b/ui/src/assets/icons/file-types/spwn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/stackblitz.svg b/ui/src/assets/icons/file-types/stackblitz.svg
new file mode 100644
index 0000000..f1806a8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stackblitz.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/stan.svg b/ui/src/assets/icons/file-types/stan.svg
new file mode 100644
index 0000000..bb5cf67
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stan.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/steadybit.svg b/ui/src/assets/icons/file-types/steadybit.svg
new file mode 100644
index 0000000..4871bbd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/steadybit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/stencil.svg b/ui/src/assets/icons/file-types/stencil.svg
new file mode 100644
index 0000000..bf8f3ea
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stencil.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/stitches.svg b/ui/src/assets/icons/file-types/stitches.svg
new file mode 100644
index 0000000..a597fbc
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stitches.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/stitches_light.svg b/ui/src/assets/icons/file-types/stitches_light.svg
new file mode 100644
index 0000000..8001d9d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stitches_light.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/storybook.svg b/ui/src/assets/icons/file-types/storybook.svg
new file mode 100644
index 0000000..8a4bdea
--- /dev/null
+++ b/ui/src/assets/icons/file-types/storybook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/stryker.svg b/ui/src/assets/icons/file-types/stryker.svg
new file mode 100644
index 0000000..05d45e6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stryker.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/stylable.svg b/ui/src/assets/icons/file-types/stylable.svg
new file mode 100644
index 0000000..be55226
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stylable.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/stylelint.svg b/ui/src/assets/icons/file-types/stylelint.svg
new file mode 100644
index 0000000..eb64524
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stylelint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/stylelint_light.svg b/ui/src/assets/icons/file-types/stylelint_light.svg
new file mode 100644
index 0000000..502fec3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stylelint_light.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/stylus.svg b/ui/src/assets/icons/file-types/stylus.svg
new file mode 100644
index 0000000..ae61b48
--- /dev/null
+++ b/ui/src/assets/icons/file-types/stylus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sublime.svg b/ui/src/assets/icons/file-types/sublime.svg
new file mode 100644
index 0000000..5c99fb9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sublime.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/subtitles.svg b/ui/src/assets/icons/file-types/subtitles.svg
new file mode 100644
index 0000000..15eebd6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/subtitles.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/supabase.svg b/ui/src/assets/icons/file-types/supabase.svg
new file mode 100644
index 0000000..78bfef7
--- /dev/null
+++ b/ui/src/assets/icons/file-types/supabase.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/svelte.svg b/ui/src/assets/icons/file-types/svelte.svg
new file mode 100644
index 0000000..4b14a6f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/svelte.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/svg.svg b/ui/src/assets/icons/file-types/svg.svg
new file mode 100644
index 0000000..fbaf9e5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/svg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/svgo.svg b/ui/src/assets/icons/file-types/svgo.svg
new file mode 100644
index 0000000..4b9cb89
--- /dev/null
+++ b/ui/src/assets/icons/file-types/svgo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/svgr.svg b/ui/src/assets/icons/file-types/svgr.svg
new file mode 100644
index 0000000..0144322
--- /dev/null
+++ b/ui/src/assets/icons/file-types/svgr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/swagger.svg b/ui/src/assets/icons/file-types/swagger.svg
new file mode 100644
index 0000000..1f79152
--- /dev/null
+++ b/ui/src/assets/icons/file-types/swagger.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/sway.svg b/ui/src/assets/icons/file-types/sway.svg
new file mode 100644
index 0000000..adca328
--- /dev/null
+++ b/ui/src/assets/icons/file-types/sway.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/swc.svg b/ui/src/assets/icons/file-types/swc.svg
new file mode 100644
index 0000000..5931bd3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/swc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/swift.svg b/ui/src/assets/icons/file-types/swift.svg
new file mode 100644
index 0000000..df413c8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/swift.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/syncpack.svg b/ui/src/assets/icons/file-types/syncpack.svg
new file mode 100644
index 0000000..9c64e31
--- /dev/null
+++ b/ui/src/assets/icons/file-types/syncpack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/systemd.svg b/ui/src/assets/icons/file-types/systemd.svg
new file mode 100644
index 0000000..943b77f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/systemd.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/systemd_light.svg b/ui/src/assets/icons/file-types/systemd_light.svg
new file mode 100644
index 0000000..39e81f6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/systemd_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/table.svg b/ui/src/assets/icons/file-types/table.svg
new file mode 100644
index 0000000..040b383
--- /dev/null
+++ b/ui/src/assets/icons/file-types/table.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tailwindcss.svg b/ui/src/assets/icons/file-types/tailwindcss.svg
new file mode 100644
index 0000000..a55450d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tailwindcss.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/taskfile.svg b/ui/src/assets/icons/file-types/taskfile.svg
new file mode 100644
index 0000000..99a775f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/taskfile.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tauri.svg b/ui/src/assets/icons/file-types/tauri.svg
new file mode 100644
index 0000000..2c7aa26
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tauri.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/taze.svg b/ui/src/assets/icons/file-types/taze.svg
new file mode 100644
index 0000000..c6e3a3f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/taze.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tcl.svg b/ui/src/assets/icons/file-types/tcl.svg
new file mode 100644
index 0000000..3c196a6
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tcl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/teal.svg b/ui/src/assets/icons/file-types/teal.svg
new file mode 100644
index 0000000..770b63d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/teal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/templ.svg b/ui/src/assets/icons/file-types/templ.svg
new file mode 100644
index 0000000..5b79cfe
--- /dev/null
+++ b/ui/src/assets/icons/file-types/templ.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/template.svg b/ui/src/assets/icons/file-types/template.svg
new file mode 100644
index 0000000..604a6f8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/template.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/terraform.svg b/ui/src/assets/icons/file-types/terraform.svg
new file mode 100644
index 0000000..d072809
--- /dev/null
+++ b/ui/src/assets/icons/file-types/terraform.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/test-js.svg b/ui/src/assets/icons/file-types/test-js.svg
new file mode 100644
index 0000000..d6ea994
--- /dev/null
+++ b/ui/src/assets/icons/file-types/test-js.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/test-jsx.svg b/ui/src/assets/icons/file-types/test-jsx.svg
new file mode 100644
index 0000000..ea2d4da
--- /dev/null
+++ b/ui/src/assets/icons/file-types/test-jsx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/test-ts.svg b/ui/src/assets/icons/file-types/test-ts.svg
new file mode 100644
index 0000000..0b4ec71
--- /dev/null
+++ b/ui/src/assets/icons/file-types/test-ts.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tex.svg b/ui/src/assets/icons/file-types/tex.svg
new file mode 100644
index 0000000..83fc24a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tex.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/textlint.svg b/ui/src/assets/icons/file-types/textlint.svg
new file mode 100644
index 0000000..a619bf0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/textlint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tilt.svg b/ui/src/assets/icons/file-types/tilt.svg
new file mode 100644
index 0000000..0ab8428
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tilt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tldraw.svg b/ui/src/assets/icons/file-types/tldraw.svg
new file mode 100644
index 0000000..c4e6d6b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tldraw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tldraw_light.svg b/ui/src/assets/icons/file-types/tldraw_light.svg
new file mode 100644
index 0000000..41faab3
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tldraw_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tobi.svg b/ui/src/assets/icons/file-types/tobi.svg
new file mode 100644
index 0000000..1a576a1
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tobi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tobimake.svg b/ui/src/assets/icons/file-types/tobimake.svg
new file mode 100644
index 0000000..0ba3b3e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tobimake.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/todo.svg b/ui/src/assets/icons/file-types/todo.svg
new file mode 100644
index 0000000..281ed65
--- /dev/null
+++ b/ui/src/assets/icons/file-types/todo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/toml.svg b/ui/src/assets/icons/file-types/toml.svg
new file mode 100644
index 0000000..aa4f24c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/toml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/toml_light.svg b/ui/src/assets/icons/file-types/toml_light.svg
new file mode 100644
index 0000000..a85712b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/toml_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/travis.svg b/ui/src/assets/icons/file-types/travis.svg
new file mode 100644
index 0000000..37a69a8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/travis.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tree.svg b/ui/src/assets/icons/file-types/tree.svg
new file mode 100644
index 0000000..a3b6d57
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tree.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/trigger.svg b/ui/src/assets/icons/file-types/trigger.svg
new file mode 100644
index 0000000..7a4f63a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/trigger.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tsconfig.svg b/ui/src/assets/icons/file-types/tsconfig.svg
new file mode 100644
index 0000000..817fb8d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tsconfig.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tsdoc.svg b/ui/src/assets/icons/file-types/tsdoc.svg
new file mode 100644
index 0000000..e7e04d0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tsdoc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tsil.svg b/ui/src/assets/icons/file-types/tsil.svg
new file mode 100644
index 0000000..261d7cd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tsil.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/tune.svg b/ui/src/assets/icons/file-types/tune.svg
new file mode 100644
index 0000000..ecbde06
--- /dev/null
+++ b/ui/src/assets/icons/file-types/tune.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/turborepo.svg b/ui/src/assets/icons/file-types/turborepo.svg
new file mode 100644
index 0000000..f0e5449
--- /dev/null
+++ b/ui/src/assets/icons/file-types/turborepo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/turborepo_light.svg b/ui/src/assets/icons/file-types/turborepo_light.svg
new file mode 100644
index 0000000..b020a35
--- /dev/null
+++ b/ui/src/assets/icons/file-types/turborepo_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/twig.svg b/ui/src/assets/icons/file-types/twig.svg
new file mode 100644
index 0000000..01f9a5d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/twig.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/twine.svg b/ui/src/assets/icons/file-types/twine.svg
new file mode 100644
index 0000000..ac1bc55
--- /dev/null
+++ b/ui/src/assets/icons/file-types/twine.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/typescript-def.svg b/ui/src/assets/icons/file-types/typescript-def.svg
new file mode 100644
index 0000000..a9ef958
--- /dev/null
+++ b/ui/src/assets/icons/file-types/typescript-def.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/typescript.svg b/ui/src/assets/icons/file-types/typescript.svg
new file mode 100644
index 0000000..acaf0dd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/typescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/typst.svg b/ui/src/assets/icons/file-types/typst.svg
new file mode 100644
index 0000000..a364734
--- /dev/null
+++ b/ui/src/assets/icons/file-types/typst.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/umi.svg b/ui/src/assets/icons/file-types/umi.svg
new file mode 100644
index 0000000..7479a4b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/umi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/uml.svg b/ui/src/assets/icons/file-types/uml.svg
new file mode 100644
index 0000000..5f70f1e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/uml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/uml_light.svg b/ui/src/assets/icons/file-types/uml_light.svg
new file mode 100644
index 0000000..c296fac
--- /dev/null
+++ b/ui/src/assets/icons/file-types/uml_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/unity.svg b/ui/src/assets/icons/file-types/unity.svg
new file mode 100644
index 0000000..f495772
--- /dev/null
+++ b/ui/src/assets/icons/file-types/unity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/unocss.svg b/ui/src/assets/icons/file-types/unocss.svg
new file mode 100644
index 0000000..eab05c4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/unocss.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/url.svg b/ui/src/assets/icons/file-types/url.svg
new file mode 100644
index 0000000..f065589
--- /dev/null
+++ b/ui/src/assets/icons/file-types/url.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/uv.svg b/ui/src/assets/icons/file-types/uv.svg
new file mode 100644
index 0000000..1549270
--- /dev/null
+++ b/ui/src/assets/icons/file-types/uv.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vagrant.svg b/ui/src/assets/icons/file-types/vagrant.svg
new file mode 100644
index 0000000..78c19f9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vagrant.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vala.svg b/ui/src/assets/icons/file-types/vala.svg
new file mode 100644
index 0000000..114aff2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vala.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vanilla-extract.svg b/ui/src/assets/icons/file-types/vanilla-extract.svg
new file mode 100644
index 0000000..c1f1e59
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vanilla-extract.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/varnish.svg b/ui/src/assets/icons/file-types/varnish.svg
new file mode 100644
index 0000000..6b504af
--- /dev/null
+++ b/ui/src/assets/icons/file-types/varnish.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vedic.svg b/ui/src/assets/icons/file-types/vedic.svg
new file mode 100644
index 0000000..3dccbeb
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vedic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/velite.svg b/ui/src/assets/icons/file-types/velite.svg
new file mode 100644
index 0000000..ca50cfa
--- /dev/null
+++ b/ui/src/assets/icons/file-types/velite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/velocity.svg b/ui/src/assets/icons/file-types/velocity.svg
new file mode 100644
index 0000000..f5fb988
--- /dev/null
+++ b/ui/src/assets/icons/file-types/velocity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vercel.svg b/ui/src/assets/icons/file-types/vercel.svg
new file mode 100644
index 0000000..8ff6e49
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vercel_light.svg b/ui/src/assets/icons/file-types/vercel_light.svg
new file mode 100644
index 0000000..314b78c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vercel_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/verdaccio.svg b/ui/src/assets/icons/file-types/verdaccio.svg
new file mode 100644
index 0000000..3b5f1d4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/verdaccio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/verified.svg b/ui/src/assets/icons/file-types/verified.svg
new file mode 100644
index 0000000..0c861c5
--- /dev/null
+++ b/ui/src/assets/icons/file-types/verified.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/verilog.svg b/ui/src/assets/icons/file-types/verilog.svg
new file mode 100644
index 0000000..c546ea8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/verilog.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vfl.svg b/ui/src/assets/icons/file-types/vfl.svg
new file mode 100644
index 0000000..3c371b4
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vfl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/video.svg b/ui/src/assets/icons/file-types/video.svg
new file mode 100644
index 0000000..2ade126
--- /dev/null
+++ b/ui/src/assets/icons/file-types/video.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vim.svg b/ui/src/assets/icons/file-types/vim.svg
new file mode 100644
index 0000000..1fc655d
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vim.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/virtual.svg b/ui/src/assets/icons/file-types/virtual.svg
new file mode 100644
index 0000000..0fdb620
--- /dev/null
+++ b/ui/src/assets/icons/file-types/virtual.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/visualstudio.svg b/ui/src/assets/icons/file-types/visualstudio.svg
new file mode 100644
index 0000000..15328de
--- /dev/null
+++ b/ui/src/assets/icons/file-types/visualstudio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vite.svg b/ui/src/assets/icons/file-types/vite.svg
new file mode 100644
index 0000000..d66cd5e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vitest.svg b/ui/src/assets/icons/file-types/vitest.svg
new file mode 100644
index 0000000..0a634e9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vitest.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vlang.svg b/ui/src/assets/icons/file-types/vlang.svg
new file mode 100644
index 0000000..17bf0e0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vlang.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/ui/src/assets/icons/file-types/vscode.svg b/ui/src/assets/icons/file-types/vscode.svg
new file mode 100644
index 0000000..bb3772a
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vscode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vue-config.svg b/ui/src/assets/icons/file-types/vue-config.svg
new file mode 100644
index 0000000..bfe01c2
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vue-config.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vue.svg b/ui/src/assets/icons/file-types/vue.svg
new file mode 100644
index 0000000..359f899
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/vuex-store.svg b/ui/src/assets/icons/file-types/vuex-store.svg
new file mode 100644
index 0000000..c98a851
--- /dev/null
+++ b/ui/src/assets/icons/file-types/vuex-store.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/wakatime.svg b/ui/src/assets/icons/file-types/wakatime.svg
new file mode 100644
index 0000000..66b8a6f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/wakatime.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/wakatime_light.svg b/ui/src/assets/icons/file-types/wakatime_light.svg
new file mode 100644
index 0000000..2b94c56
--- /dev/null
+++ b/ui/src/assets/icons/file-types/wakatime_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/wallaby.svg b/ui/src/assets/icons/file-types/wallaby.svg
new file mode 100644
index 0000000..0e7ce6e
--- /dev/null
+++ b/ui/src/assets/icons/file-types/wallaby.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/wally.svg b/ui/src/assets/icons/file-types/wally.svg
new file mode 100644
index 0000000..a5c1f24
--- /dev/null
+++ b/ui/src/assets/icons/file-types/wally.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/watchman.svg b/ui/src/assets/icons/file-types/watchman.svg
new file mode 100644
index 0000000..74773cd
--- /dev/null
+++ b/ui/src/assets/icons/file-types/watchman.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/webassembly.svg b/ui/src/assets/icons/file-types/webassembly.svg
new file mode 100644
index 0000000..69a43aa
--- /dev/null
+++ b/ui/src/assets/icons/file-types/webassembly.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/webhint.svg b/ui/src/assets/icons/file-types/webhint.svg
new file mode 100644
index 0000000..fdaa668
--- /dev/null
+++ b/ui/src/assets/icons/file-types/webhint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/webpack.svg b/ui/src/assets/icons/file-types/webpack.svg
new file mode 100644
index 0000000..68233d9
--- /dev/null
+++ b/ui/src/assets/icons/file-types/webpack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/wepy.svg b/ui/src/assets/icons/file-types/wepy.svg
new file mode 100644
index 0000000..bed1ad0
--- /dev/null
+++ b/ui/src/assets/icons/file-types/wepy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/werf.svg b/ui/src/assets/icons/file-types/werf.svg
new file mode 100644
index 0000000..7a89a1f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/werf.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/windicss.svg b/ui/src/assets/icons/file-types/windicss.svg
new file mode 100644
index 0000000..4f31c55
--- /dev/null
+++ b/ui/src/assets/icons/file-types/windicss.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/wolframlanguage.svg b/ui/src/assets/icons/file-types/wolframlanguage.svg
new file mode 100644
index 0000000..77e8809
--- /dev/null
+++ b/ui/src/assets/icons/file-types/wolframlanguage.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/word.svg b/ui/src/assets/icons/file-types/word.svg
new file mode 100644
index 0000000..a90b88f
--- /dev/null
+++ b/ui/src/assets/icons/file-types/word.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/wrangler.svg b/ui/src/assets/icons/file-types/wrangler.svg
new file mode 100644
index 0000000..51a7983
--- /dev/null
+++ b/ui/src/assets/icons/file-types/wrangler.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/wxt.svg b/ui/src/assets/icons/file-types/wxt.svg
new file mode 100644
index 0000000..d43b742
--- /dev/null
+++ b/ui/src/assets/icons/file-types/wxt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/xaml.svg b/ui/src/assets/icons/file-types/xaml.svg
new file mode 100644
index 0000000..0b7e865
--- /dev/null
+++ b/ui/src/assets/icons/file-types/xaml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/xmake.svg b/ui/src/assets/icons/file-types/xmake.svg
new file mode 100644
index 0000000..47b3ce8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/xmake.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/xml.svg b/ui/src/assets/icons/file-types/xml.svg
new file mode 100644
index 0000000..c3a1eaf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/xml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/yaml.svg b/ui/src/assets/icons/file-types/yaml.svg
new file mode 100644
index 0000000..1f1cc7c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/yaml.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/yang.svg b/ui/src/assets/icons/file-types/yang.svg
new file mode 100644
index 0000000..fba4bbf
--- /dev/null
+++ b/ui/src/assets/icons/file-types/yang.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/yarn.svg b/ui/src/assets/icons/file-types/yarn.svg
new file mode 100644
index 0000000..9af575c
--- /dev/null
+++ b/ui/src/assets/icons/file-types/yarn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/zeabur.svg b/ui/src/assets/icons/file-types/zeabur.svg
new file mode 100644
index 0000000..37b0ea8
--- /dev/null
+++ b/ui/src/assets/icons/file-types/zeabur.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/zeabur_light.svg b/ui/src/assets/icons/file-types/zeabur_light.svg
new file mode 100644
index 0000000..0d01f2b
--- /dev/null
+++ b/ui/src/assets/icons/file-types/zeabur_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/zig.svg b/ui/src/assets/icons/file-types/zig.svg
new file mode 100644
index 0000000..b5604df
--- /dev/null
+++ b/ui/src/assets/icons/file-types/zig.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/file-types/zip.svg b/ui/src/assets/icons/file-types/zip.svg
new file mode 100644
index 0000000..1056c60
--- /dev/null
+++ b/ui/src/assets/icons/file-types/zip.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/provider-logos/bailian-coding-plan.svg b/ui/src/assets/provider-logos/bailian-coding-plan.svg
new file mode 100644
index 0000000..b3a2edc
--- /dev/null
+++ b/ui/src/assets/provider-logos/bailian-coding-plan.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/ui/src/assets/provider-logos/evroc.svg b/ui/src/assets/provider-logos/evroc.svg
new file mode 100644
index 0000000..c7910df
--- /dev/null
+++ b/ui/src/assets/provider-logos/evroc.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/ui/src/assets/provider-logos/gocode.svg b/ui/src/assets/provider-logos/gocode.svg
new file mode 100644
index 0000000..7af2fbd
--- /dev/null
+++ b/ui/src/assets/provider-logos/gocode.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/ui/src/components/auth/SessionAuthGate.tsx b/ui/src/components/auth/SessionAuthGate.tsx
new file mode 100644
index 0000000..60a0f8f
--- /dev/null
+++ b/ui/src/components/auth/SessionAuthGate.tsx
@@ -0,0 +1,338 @@
+import React from 'react';
+import { RiLockLine, RiLockUnlockLine, RiLoader4Line } from '@remixicon/react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
+import { syncDesktopSettings, initializeAppearancePreferences } from '@/lib/persistence';
+import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence';
+import { DesktopHostSwitcherInline } from '@/components/desktop/DesktopHostSwitcher';
+import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
+
+const STATUS_CHECK_ENDPOINT = '/auth/session';
+
+const fetchSessionStatus = async (): Promise => {
+ console.log('[Frontend Auth] Checking session status...');
+ const response = await fetch(STATUS_CHECK_ENDPOINT, {
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ Accept: 'application/json',
+ },
+ });
+ console.log('[Frontend Auth] Session status response:', response.status, response.statusText);
+ return response;
+};
+
+const submitPassword = async (password: string): Promise => {
+ console.log('[Frontend Auth] Submitting password...');
+ const response = await fetch(STATUS_CHECK_ENDPOINT, {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ body: JSON.stringify({ password }),
+ });
+ console.log('[Frontend Auth] Password submit response:', response.status, response.statusText);
+ return response;
+};
+
+const AuthShell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+);
+
+const LoadingScreen: React.FC = () => (
+
+
+
+);
+
+const ErrorScreen: React.FC = ({ onRetry, errorType = 'network', retryAfter }) => {
+ const isRateLimit = errorType === 'rate-limit';
+ const minutes = retryAfter ? Math.ceil(retryAfter / 60) : 1;
+
+ return (
+
+
+
+
+ {isRateLimit ? 'Too many attempts' : 'Unable to reach server'}
+
+
+ {isRateLimit
+ ? `Please wait ${minutes} minute${minutes > 1 ? 's' : ''} before trying again.`
+ : "We couldn't verify the UI session. Check that the service is running and try again."}
+
+
+
+ Retry
+
+
+
+ );
+};
+
+interface SessionAuthGateProps {
+ children: React.ReactNode;
+}
+
+type GateState = 'pending' | 'authenticated' | 'locked' | 'error' | 'rate-limited';
+
+interface ErrorScreenProps {
+ onRetry: () => void;
+ errorType?: 'network' | 'rate-limit';
+ retryAfter?: number;
+}
+
+export const SessionAuthGate: React.FC = ({ children }) => {
+ const vscodeRuntime = React.useMemo(() => isVSCodeRuntime(), []);
+ const skipAuth = vscodeRuntime;
+ const showHostSwitcher = React.useMemo(() => isDesktopShell() && !vscodeRuntime, [vscodeRuntime]);
+ const [state, setState] = React.useState(() => (skipAuth ? 'authenticated' : 'pending'));
+ const [password, setPassword] = React.useState('');
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+ const [errorMessage, setErrorMessage] = React.useState('');
+ const [retryAfter, setRetryAfter] = React.useState(undefined);
+ const passwordInputRef = React.useRef(null);
+ const hasResyncedRef = React.useRef(skipAuth);
+
+ const checkStatus = React.useCallback(async () => {
+ if (skipAuth) {
+ console.log('[Frontend Auth] VSCode runtime, skipping auth');
+ setState('authenticated');
+ return;
+ }
+
+ // 检查 cookie 是否存在
+ const cookies = document.cookie;
+ const hasAccessToken = cookies.includes('oc_ui_session=');
+ const hasRefreshToken = cookies.includes('oc_ui_refresh=');
+ console.log('[Frontend Auth] Cookies check - access:', hasAccessToken, 'refresh:', hasRefreshToken);
+ console.log('[Frontend Auth] All cookies:', cookies.split(';').map(c => c.trim().split('=')[0]));
+
+ setState((prev) => (prev === 'authenticated' ? prev : 'pending'));
+ try {
+ const response = await fetchSessionStatus();
+ const responseText = await response.text();
+ console.log('[Frontend Auth] Raw response:', response.status, responseText);
+
+ if (response.ok) {
+ console.log('[Frontend Auth] Session is authenticated');
+ setState('authenticated');
+ setErrorMessage('');
+ setRetryAfter(undefined);
+ return;
+ }
+ if (response.status === 401) {
+ console.warn('[Frontend Auth] Session is locked (401)');
+ setState('locked');
+ setRetryAfter(undefined);
+ return;
+ }
+ if (response.status === 429) {
+ let data: { retryAfter?: number } = {};
+ try {
+ data = JSON.parse(responseText);
+ } catch {
+ data = {};
+ }
+ setRetryAfter(data.retryAfter);
+ setState('rate-limited');
+ return;
+ }
+ console.error('[Frontend Auth] Unexpected response status:', response.status);
+ setState('error');
+ } catch (error) {
+ console.warn('Failed to check session status:', error);
+ setState('error');
+ }
+ }, [skipAuth]);
+
+ React.useEffect(() => {
+ if (skipAuth) {
+ return;
+ }
+ void checkStatus();
+ }, [checkStatus, skipAuth]);
+
+ React.useEffect(() => {
+ if (!skipAuth && state === 'locked') {
+ hasResyncedRef.current = false;
+ }
+ }, [skipAuth, state]);
+
+ React.useEffect(() => {
+ if (state === 'locked' && passwordInputRef.current) {
+ passwordInputRef.current.focus();
+ passwordInputRef.current.select();
+ }
+ }, [state]);
+
+ React.useEffect(() => {
+ if (skipAuth) {
+ return;
+ }
+ if (state === 'authenticated' && !hasResyncedRef.current) {
+ hasResyncedRef.current = true;
+ void (async () => {
+ await syncDesktopSettings();
+ await initializeAppearancePreferences();
+ await applyPersistedDirectoryPreferences();
+ })();
+ }
+ }, [skipAuth, state]);
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ if (!password || isSubmitting) {
+ return;
+ }
+
+ setIsSubmitting(true);
+ setErrorMessage('');
+
+ try {
+ const response = await submitPassword(password);
+ if (response.ok) {
+ console.log('[Frontend Auth] Login successful');
+ const cookies = document.cookie;
+ const hasAccessToken = cookies.includes('oc_ui_session=');
+ const hasRefreshToken = cookies.includes('oc_ui_refresh=');
+ console.log('[Frontend Auth] After login - access:', hasAccessToken, 'refresh:', hasRefreshToken);
+ console.log('[Frontend Auth] All cookies after login:', cookies.split(';').map(c => c.trim().split('=')[0]).filter(Boolean));
+ setPassword('');
+ setState('authenticated');
+ return;
+ }
+
+ if (response.status === 401) {
+ console.warn('[Frontend Auth] Login failed: Invalid password');
+ setErrorMessage('Incorrect password. Try again.');
+ setState('locked');
+ return;
+ }
+
+ if (response.status === 429) {
+ console.warn('[Frontend Auth] Login failed: Rate limited');
+ const data = await response.json().catch(() => ({}));
+ setRetryAfter(data.retryAfter);
+ setState('rate-limited');
+ return;
+ }
+
+ console.error('[Frontend Auth] Login failed: Unexpected response', response.status);
+ setErrorMessage('Unexpected response from server.');
+ setState('error');
+ } catch (error) {
+ console.warn('Failed to submit UI password:', error);
+ setErrorMessage('Network error. Check connection and retry.');
+ setState('error');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (state === 'pending') {
+ return ;
+ }
+
+ if (state === 'error') {
+ return void checkStatus()} errorType="network" />;
+ }
+
+ if (state === 'rate-limited') {
+ return void checkStatus()} errorType="rate-limit" retryAfter={retryAfter} />;
+ }
+
+ if (state === 'locked') {
+ return (
+
+
+
+
+ Unlock OpenChamber
+
+
+ This session is password-protected.
+
+
+
+
+
+ {showHostSwitcher && (
+
+
+
+ Use Local if remote is unreachable.
+
+
+ )}
+
+
+ );
+ }
+
+ return <>{children}>;
+};
diff --git a/ui/src/components/chat/AgentMentionAutocomplete.tsx b/ui/src/components/chat/AgentMentionAutocomplete.tsx
new file mode 100644
index 0000000..3a1aa39
--- /dev/null
+++ b/ui/src/components/chat/AgentMentionAutocomplete.tsx
@@ -0,0 +1,245 @@
+import React from 'react';
+import { cn, fuzzyMatch } from '@/lib/utils';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useAgentsStore, isAgentBuiltIn, type AgentWithExtras } from '@/stores/useAgentsStore';
+import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
+
+interface AgentInfo {
+ name: string;
+ description?: string;
+ mode?: string | null;
+ scope?: string;
+ isBuiltIn?: boolean;
+}
+
+export interface AgentMentionAutocompleteHandle {
+ handleKeyDown: (key: string) => void;
+}
+
+type AutocompleteTab = 'commands' | 'agents' | 'files';
+
+const isMentionableAgentMode = (mode?: string | null): boolean => {
+ if (!mode) return false;
+ return mode !== 'primary';
+};
+
+interface AgentMentionAutocompleteProps {
+ searchQuery: string;
+ onAgentSelect: (agentName: string) => void;
+ onClose: () => void;
+ showTabs?: boolean;
+ activeTab?: AutocompleteTab;
+ onTabSelect?: (tab: AutocompleteTab) => void;
+}
+
+export const AgentMentionAutocomplete = React.forwardRef(({
+ searchQuery,
+ onAgentSelect,
+ onClose,
+ showTabs,
+ activeTab = 'agents',
+ onTabSelect,
+}, ref) => {
+ const containerRef = React.useRef(null);
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
+ const [agents, setAgents] = React.useState([]);
+ const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
+ const ignoreTabClickRef = React.useRef(false);
+ const { getVisibleAgents } = useConfigStore();
+ const { agents: agentsWithMetadata, loadAgents } = useAgentsStore();
+
+ React.useEffect(() => {
+ if (agentsWithMetadata.length === 0) {
+ void loadAgents();
+ }
+ }, [loadAgents, agentsWithMetadata.length]);
+
+ React.useEffect(() => {
+ const visibleAgents = getVisibleAgents();
+ const filtered = visibleAgents
+ .filter((agent) => isMentionableAgentMode(agent.mode))
+ .map((agent) => {
+ const metadata = agentsWithMetadata.find(a => a.name === agent.name) as (AgentWithExtras & { scope?: string }) | undefined;
+ return {
+ name: agent.name,
+ description: agent.description,
+ mode: agent.mode ?? undefined,
+ scope: metadata?.scope,
+ isBuiltIn: metadata ? isAgentBuiltIn(metadata) : false,
+ };
+ });
+
+ const normalizedQuery = searchQuery.trim();
+ const matches = normalizedQuery.length
+ ? filtered.filter((agent) => fuzzyMatch(agent.name, normalizedQuery))
+ : filtered;
+
+ matches.sort((a, b) => a.name.localeCompare(b.name));
+
+ setAgents(matches);
+ setSelectedIndex(0);
+ }, [getVisibleAgents, searchQuery, agentsWithMetadata]);
+
+ React.useEffect(() => {
+ itemRefs.current[selectedIndex]?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ });
+ }, [selectedIndex]);
+
+ React.useEffect(() => {
+ const handlePointerDown = (event: MouseEvent | TouchEvent) => {
+ const target = event.target as Node | null;
+ if (!target || !containerRef.current) {
+ return;
+ }
+ if (!containerRef.current.contains(target)) {
+ onClose();
+ }
+ };
+
+ document.addEventListener('pointerdown', handlePointerDown, true);
+ return () => {
+ document.removeEventListener('pointerdown', handlePointerDown, true);
+ };
+ }, [onClose]);
+
+ React.useImperativeHandle(ref, () => ({
+ handleKeyDown: (key: string) => {
+ if (key === 'Escape') {
+ onClose();
+ return;
+ }
+
+ if (!agents.length) {
+ return;
+ }
+
+ if (key === 'ArrowDown') {
+ setSelectedIndex((prev) => (prev + 1) % agents.length);
+ return;
+ }
+
+ if (key === 'ArrowUp') {
+ setSelectedIndex((prev) => (prev - 1 + agents.length) % agents.length);
+ return;
+ }
+
+ if (key === 'Enter' || key === 'Tab') {
+ const agent = agents[(selectedIndex + agents.length) % agents.length];
+ if (agent) {
+ onAgentSelect(agent.name);
+ }
+ }
+ },
+ }), [agents, onAgentSelect, onClose, selectedIndex]);
+
+ const renderAgent = (agent: AgentInfo, index: number) => {
+ const isSystem = agent.isBuiltIn;
+ const isProject = agent.scope === 'project';
+
+ return (
+ {
+ itemRefs.current[index] = el;
+ }}
+ className={cn(
+ 'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
+ index === selectedIndex && 'bg-interactive-selection'
+ )}
+ onClick={() => onAgentSelect(agent.name)}
+ onMouseEnter={() => setSelectedIndex(index)}
+ >
+
+
+ #{agent.name}
+ {isSystem ? (
+
+ system
+
+ ) : agent.scope ? (
+
+ {agent.scope}
+
+ ) : null}
+
+ {agent.description && (
+
+ {agent.description}
+
+ )}
+
+
+ );
+ };
+
+ return (
+
+ {showTabs ? (
+
+
+ {([
+ { id: 'commands' as const, label: 'Commands' },
+ { id: 'agents' as const, label: 'Agents' },
+ { id: 'files' as const, label: 'Files' },
+ ]).map((tab) => (
+ {
+ if (event.pointerType !== 'touch') {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ ignoreTabClickRef.current = true;
+ onTabSelect?.(tab.id);
+ }}
+ onClick={() => {
+ if (ignoreTabClickRef.current) {
+ ignoreTabClickRef.current = false;
+ return;
+ }
+ onTabSelect?.(tab.id);
+ }}
+ >
+ {tab.label}
+
+ ))}
+
+
+ ) : null}
+
+ {agents.length ? (
+
+ {agents.map((agent, index) => renderAgent(agent, index))}
+
+ ) : (
+
+ No agents found
+
+ )}
+
+
+ ↑↓ navigate • Enter select • Esc close
+
+
+ );
+});
+
+AgentMentionAutocomplete.displayName = 'AgentMentionAutocomplete';
diff --git a/ui/src/components/chat/ChatContainer.tsx b/ui/src/components/chat/ChatContainer.tsx
new file mode 100644
index 0000000..aed1670
--- /dev/null
+++ b/ui/src/components/chat/ChatContainer.tsx
@@ -0,0 +1,686 @@
+import React from 'react';
+import { RiArrowDownLine, RiArrowLeftLine } from '@remixicon/react';
+import { useShallow } from 'zustand/react/shallow';
+import type { Message, Part } from '@opencode-ai/sdk/v2';
+
+import { ChatInput } from './ChatInput';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { Skeleton } from '@/components/ui/skeleton';
+import ChatEmptyState from './ChatEmptyState';
+import MessageList, { type MessageListHandle } from './MessageList';
+import { ScrollShadow } from '@/components/ui/ScrollShadow';
+import { useChatScrollManager } from '@/hooks/useChatScrollManager';
+import { useDeviceInfo } from '@/lib/device';
+import { getMemoryLimits } from '@/stores/types/sessionTypes';
+import { Button } from '@/components/ui/button';
+import { ButtonSmall } from '@/components/ui/button-small';
+import { OverlayScrollbar } from '@/components/ui/OverlayScrollbar';
+import { TimelineDialog } from './TimelineDialog';
+import type { PermissionRequest } from '@/types/permission';
+import type { QuestionRequest } from '@/types/question';
+import { cn } from '@/lib/utils';
+
+const EMPTY_MESSAGES: Array<{ info: Message; parts: Part[] }> = [];
+const EMPTY_PERMISSIONS: PermissionRequest[] = [];
+const EMPTY_QUESTIONS: QuestionRequest[] = [];
+const IDLE_SESSION_STATUS = { type: 'idle' as const };
+
+const collectVisibleSessionIdsForBlockingRequests = (
+ sessions: Array<{ id: string; parentID?: string }> | undefined,
+ currentSessionId: string | null
+): string[] => {
+ if (!currentSessionId) return [];
+ if (!Array.isArray(sessions) || sessions.length === 0) return [currentSessionId];
+
+ const current = sessions.find((session) => session.id === currentSessionId);
+ if (!current) return [currentSessionId];
+
+ // Opencode parity: when viewing a child session, permission/question prompts are handled in parent thread.
+ if (current.parentID) {
+ return [];
+ }
+
+ const childIds = sessions
+ .filter((session) => session.parentID === currentSessionId)
+ .map((session) => session.id);
+
+ return [currentSessionId, ...childIds];
+};
+
+const flattenBlockingRequests = (
+ source: Map,
+ sessionIds: string[]
+): T[] => {
+ if (sessionIds.length === 0) return [];
+ const seen = new Set();
+ const result: T[] = [];
+
+ for (const sessionId of sessionIds) {
+ const entries = source.get(sessionId);
+ if (!entries || entries.length === 0) continue;
+ for (const entry of entries) {
+ if (seen.has(entry.id)) continue;
+ seen.add(entry.id);
+ result.push(entry);
+ }
+ }
+
+ return result;
+};
+
+export const ChatContainer: React.FC = () => {
+ const {
+ currentSessionId,
+ isLoading,
+ loadMessages,
+ loadMoreMessages,
+ updateViewportAnchor,
+ openNewSessionDraft,
+ setCurrentSession,
+ trimToViewportWindow,
+ newSessionDraft,
+ } = useSessionStore(
+ useShallow((state) => ({
+ currentSessionId: state.currentSessionId,
+ isLoading: state.isLoading,
+ loadMessages: state.loadMessages,
+ loadMoreMessages: state.loadMoreMessages,
+ updateViewportAnchor: state.updateViewportAnchor,
+ openNewSessionDraft: state.openNewSessionDraft,
+ setCurrentSession: state.setCurrentSession,
+ trimToViewportWindow: state.trimToViewportWindow,
+ newSessionDraft: state.newSessionDraft,
+ }))
+ );
+
+ const { isSyncing, messageStreamStates, sessionMemoryStateMap } = useSessionStore(
+ useShallow((state) => ({
+ isSyncing: state.isSyncing,
+ messageStreamStates: state.messageStreamStates,
+ sessionMemoryStateMap: state.sessionMemoryState,
+ }))
+ );
+
+ const {
+ isTimelineDialogOpen,
+ setTimelineDialogOpen,
+ isExpandedInput,
+ stickyUserHeader,
+ } = useUIStore();
+
+ const sessionMessages = useSessionStore(
+ React.useCallback(
+ (state) => (currentSessionId ? state.messages.get(currentSessionId) ?? EMPTY_MESSAGES : EMPTY_MESSAGES),
+ [currentSessionId]
+ )
+ );
+
+ const sessions = useSessionStore((state) => state.sessions);
+
+ const blockingRequestState = useSessionStore(
+ useShallow((state) => ({
+ sessions: state.sessions,
+ permissions: state.permissions,
+ questions: state.questions,
+ }))
+ );
+
+ const scopedSessionIds = React.useMemo(
+ () => collectVisibleSessionIdsForBlockingRequests(
+ blockingRequestState.sessions.map((session) => ({ id: session.id, parentID: session.parentID })),
+ currentSessionId,
+ ),
+ [blockingRequestState.sessions, currentSessionId]
+ );
+
+ const sessionPermissions = React.useMemo(() => {
+ if (scopedSessionIds.length === 0) return EMPTY_PERMISSIONS;
+ return flattenBlockingRequests(blockingRequestState.permissions, scopedSessionIds);
+ }, [blockingRequestState.permissions, scopedSessionIds]);
+
+ const sessionQuestions = React.useMemo(() => {
+ if (scopedSessionIds.length === 0) return EMPTY_QUESTIONS;
+ return flattenBlockingRequests(blockingRequestState.questions, scopedSessionIds);
+ }, [blockingRequestState.questions, scopedSessionIds]);
+
+ const memoryState = useSessionStore(
+ React.useCallback(
+ (state) => (currentSessionId ? state.sessionMemoryState.get(currentSessionId) ?? null : null),
+ [currentSessionId]
+ )
+ );
+
+ const streamingMessageId = useSessionStore(
+ React.useCallback(
+ (state) => (currentSessionId ? state.streamingMessageIds.get(currentSessionId) ?? null : null),
+ [currentSessionId]
+ )
+ );
+
+ const sessionStatusForCurrent = useSessionStore(
+ React.useCallback(
+ (state) => (currentSessionId ? state.sessionStatus?.get(currentSessionId) ?? IDLE_SESSION_STATUS : IDLE_SESSION_STATUS),
+ [currentSessionId]
+ )
+ );
+
+ const hasSessionMessagesEntry = useSessionStore(
+ React.useCallback((state) => (currentSessionId ? state.messages.has(currentSessionId) : false), [currentSessionId])
+ );
+
+ const { isMobile } = useDeviceInfo();
+ const draftOpen = Boolean(newSessionDraft?.open);
+ const isDesktopExpandedInput = isExpandedInput && !isMobile;
+ const messageListRef = React.useRef(null);
+
+ const parentSession = React.useMemo(() => {
+ if (!currentSessionId) {
+ return null;
+ }
+
+ const current = sessions.find((session) => session.id === currentSessionId);
+ const parentID = current?.parentID;
+ if (!parentID) {
+ return null;
+ }
+
+ return sessions.find((session) => session.id === parentID) ?? null;
+ }, [currentSessionId, sessions]);
+
+ const handleReturnToParentSession = React.useCallback(() => {
+ if (!parentSession) {
+ return;
+ }
+ void setCurrentSession(parentSession.id);
+ }, [parentSession, setCurrentSession]);
+
+ const returnToParentButton = parentSession ? (
+
+
+ Parent
+
+ ) : null;
+
+ React.useEffect(() => {
+ if (!currentSessionId && !draftOpen) {
+ openNewSessionDraft();
+ }
+ }, [currentSessionId, draftOpen, openNewSessionDraft]);
+
+ const [turnStart, setTurnStart] = React.useState(0);
+ const turnHandleRef = React.useRef(null);
+ const turnIdleRef = React.useRef(false);
+ const initializedTurnStartSessionRef = React.useRef(null);
+ const TURN_INIT = 5;
+ const TURN_BATCH = 8;
+
+ const userTurnIndexes = React.useMemo(() => {
+ const indexes: number[] = [];
+ for (let i = 0; i < sessionMessages.length; i += 1) {
+ const message = sessionMessages[i];
+ const role = (message.info as { clientRole?: string | null | undefined }).clientRole ?? message.info.role;
+ if (role === 'user') {
+ indexes.push(i);
+ }
+ }
+ return indexes;
+ }, [sessionMessages]);
+
+ const cancelTurnBackfill = React.useCallback(() => {
+ const handle = turnHandleRef.current;
+ if (handle === null) {
+ return;
+ }
+ turnHandleRef.current = null;
+ if (turnIdleRef.current && typeof window !== 'undefined' && typeof window.cancelIdleCallback === 'function') {
+ window.cancelIdleCallback(handle);
+ return;
+ }
+ if (typeof window !== 'undefined') {
+ window.clearTimeout(handle);
+ }
+ }, []);
+
+ const renderedSessionMessages = React.useMemo(() => {
+ if (turnStart <= 0 || userTurnIndexes.length === 0) {
+ return sessionMessages;
+ }
+ const startIndex = userTurnIndexes[turnStart] ?? 0;
+ return sessionMessages.slice(startIndex);
+ }, [sessionMessages, turnStart, userTurnIndexes]);
+
+ const backfillTurns = React.useCallback(() => {
+ if (turnStart <= 0) {
+ return;
+ }
+
+ const container = typeof document !== 'undefined'
+ ? (document.querySelector('[data-scrollbar="chat"]') as HTMLDivElement | null)
+ : null;
+ const beforeTop = container?.scrollTop ?? null;
+ const beforeHeight = container?.scrollHeight ?? null;
+
+ setTurnStart((prev) => (prev - TURN_BATCH > 0 ? prev - TURN_BATCH : 0));
+
+ if (container && beforeTop !== null && beforeHeight !== null) {
+ window.requestAnimationFrame(() => {
+ const delta = container.scrollHeight - beforeHeight;
+ if (delta !== 0) {
+ container.scrollTop = beforeTop + delta;
+ }
+ });
+ }
+ }, [turnStart]);
+
+ const scheduleTurnBackfill = React.useCallback(() => {
+ if (turnHandleRef.current !== null || turnStart <= 0) {
+ return;
+ }
+
+ if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') {
+ turnIdleRef.current = true;
+ turnHandleRef.current = window.requestIdleCallback(() => {
+ turnHandleRef.current = null;
+ backfillTurns();
+ });
+ return;
+ }
+
+ turnIdleRef.current = false;
+ turnHandleRef.current = window.setTimeout(() => {
+ turnHandleRef.current = null;
+ backfillTurns();
+ }, 0);
+ }, [backfillTurns, turnStart]);
+
+ const sessionBlockingCards = React.useMemo(() => {
+ return [...sessionPermissions, ...sessionQuestions];
+ }, [sessionPermissions, sessionQuestions]);
+
+ const {
+ scrollRef,
+ handleMessageContentChange,
+ getAnimationHandlers,
+ showScrollButton,
+ scrollToBottom,
+ scrollToPosition,
+ isPinned,
+ } = useChatScrollManager({
+ currentSessionId,
+ sessionMessages: renderedSessionMessages,
+ streamingMessageId,
+ sessionMemoryState: sessionMemoryStateMap,
+ updateViewportAnchor,
+ isSyncing,
+ isMobile,
+ messageStreamStates,
+ sessionPermissions: sessionBlockingCards,
+ trimToViewportWindow,
+ });
+
+ React.useLayoutEffect(() => {
+ const container = scrollRef.current;
+ if (!container) {
+ return;
+ }
+
+ const updateChatScrollHeight = () => {
+ container.style.setProperty('--chat-scroll-height', `${container.clientHeight}px`);
+ };
+
+ updateChatScrollHeight();
+
+ if (typeof ResizeObserver === 'undefined') {
+ window.addEventListener('resize', updateChatScrollHeight);
+ return () => {
+ window.removeEventListener('resize', updateChatScrollHeight);
+ };
+ }
+
+ const resizeObserver = new ResizeObserver(updateChatScrollHeight);
+ resizeObserver.observe(container);
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, [currentSessionId, isDesktopExpandedInput, scrollRef]);
+
+ React.useEffect(() => {
+ cancelTurnBackfill();
+ if (!currentSessionId) {
+ initializedTurnStartSessionRef.current = null;
+ setTurnStart(0);
+ return;
+ }
+
+ if (initializedTurnStartSessionRef.current === currentSessionId) {
+ return;
+ }
+
+ if (sessionMessages.length === 0) {
+ setTurnStart(0);
+ return;
+ }
+
+ const turnCount = userTurnIndexes.length;
+ const start = turnCount > TURN_INIT ? turnCount - TURN_INIT : 0;
+ setTurnStart(start);
+ initializedTurnStartSessionRef.current = currentSessionId;
+ }, [cancelTurnBackfill, currentSessionId, sessionMessages.length, userTurnIndexes.length]);
+
+ const isSessionActive = sessionStatusForCurrent.type === 'busy' || sessionStatusForCurrent.type === 'retry';
+
+ React.useEffect(() => {
+ if (isSessionActive) {
+ cancelTurnBackfill();
+ return;
+ }
+ scheduleTurnBackfill();
+ return () => {
+ cancelTurnBackfill();
+ };
+ }, [cancelTurnBackfill, isSessionActive, scheduleTurnBackfill, turnStart]);
+
+ const hasMoreAbove = React.useMemo(() => {
+ if (!memoryState) {
+ return sessionMessages.length >= getMemoryLimits().HISTORICAL_MESSAGES;
+ }
+ if (memoryState.historyComplete === true) {
+ return false;
+ }
+ if (memoryState.hasMoreAbove) {
+ return true;
+ }
+ if (memoryState.historyComplete === false) {
+ return true;
+ }
+
+ // Backward compatibility: older persisted sessions may miss history flags.
+ if (memoryState.hasMoreAbove === undefined && memoryState.historyComplete === undefined) {
+ return sessionMessages.length >= getMemoryLimits().HISTORICAL_MESSAGES;
+ }
+
+ return false;
+ }, [memoryState, sessionMessages.length]);
+
+ const hasHistoryMetadata = React.useMemo(() => {
+ if (!memoryState) {
+ return false;
+ }
+ return memoryState.hasMoreAbove !== undefined || memoryState.historyComplete !== undefined;
+ }, [memoryState]);
+ const [isLoadingOlder, setIsLoadingOlder] = React.useState(false);
+ React.useEffect(() => {
+ setIsLoadingOlder(false);
+ }, [currentSessionId]);
+
+ const handleLoadOlder = React.useCallback(async () => {
+ if (!currentSessionId || isLoadingOlder) {
+ return;
+ }
+
+ cancelTurnBackfill();
+ setTurnStart(0);
+
+ const container = scrollRef.current;
+ const anchor = messageListRef.current?.captureViewportAnchor() ?? null;
+ const prevHeight = container?.scrollHeight ?? null;
+ const prevTop = container?.scrollTop ?? null;
+
+ setIsLoadingOlder(true);
+ void loadMoreMessages(currentSessionId, 'up')
+ .then(() => {
+ const restored = anchor ? (messageListRef.current?.restoreViewportAnchor(anchor) ?? false) : false;
+ if (!restored && container && prevHeight !== null && prevTop !== null) {
+ const heightDiff = container.scrollHeight - prevHeight;
+ scrollToPosition(prevTop + heightDiff, { instant: true });
+ }
+ })
+ .finally(() => {
+ setIsLoadingOlder(false);
+ });
+ }, [cancelTurnBackfill, currentSessionId, isLoadingOlder, loadMoreMessages, scrollRef, scrollToPosition]);
+
+ const handleRenderEarlier = React.useCallback(() => {
+ cancelTurnBackfill();
+ setTurnStart(0);
+ }, [cancelTurnBackfill]);
+
+ // Scroll to a specific message by ID (for timeline dialog)
+ const scrollToMessage = React.useCallback((messageId: string) => {
+ if (messageListRef.current?.scrollToMessageId(messageId, { behavior: 'smooth' })) {
+ return;
+ }
+
+ const container = scrollRef.current;
+ if (!container) return;
+
+ // Find the message element by looking for data-message-id attribute
+ const messageElement = container.querySelector(`[data-message-id="${messageId}"]`) as HTMLElement;
+ if (messageElement) {
+ // Scroll to the message with some padding (50px from top)
+ const containerRect = container.getBoundingClientRect();
+ const messageRect = messageElement.getBoundingClientRect();
+ const offset = 50;
+
+ const scrollTop = messageRect.top - containerRect.top + container.scrollTop - offset;
+ container.scrollTo({
+ top: scrollTop,
+ behavior: 'smooth'
+ });
+ }
+ }, [scrollRef]);
+
+ React.useEffect(() => {
+ if (!currentSessionId) {
+ return;
+ }
+
+ const hasSessionMessages = hasSessionMessagesEntry;
+ if (hasSessionMessages && hasHistoryMetadata) {
+ return;
+ }
+
+ const load = async () => {
+ await loadMessages(currentSessionId).finally(() => {
+ const statusType = sessionStatusForCurrent.type ?? 'idle';
+ const isActivePhase = statusType === 'busy' || statusType === 'retry';
+ const shouldSkipScroll = isActivePhase && isPinned;
+
+ if (!shouldSkipScroll) {
+ if (typeof window === 'undefined') {
+ scrollToBottom({ instant: true });
+ } else {
+ window.requestAnimationFrame(() => {
+ scrollToBottom({ instant: true });
+ });
+ }
+ }
+ });
+ };
+
+ void load();
+ }, [currentSessionId, hasHistoryMetadata, hasSessionMessagesEntry, isPinned, loadMessages, scrollToBottom, sessionMessages.length, sessionStatusForCurrent.type]);
+
+ if (!currentSessionId && !draftOpen) {
+ return (
+
+
+
+ );
+ }
+
+ if (!currentSessionId && draftOpen) {
+ return (
+
+ {!isDesktopExpandedInput ? (
+
+
+
+ ) : null}
+
+
+
+
+ );
+ }
+
+ if (!currentSessionId) {
+ return null;
+ }
+
+ if (isLoading && sessionMessages.length === 0 && !streamingMessageId) {
+ const hasMessagesEntry = hasSessionMessagesEntry;
+ if (!hasMessagesEntry) {
+ return (
+
+ {returnToParentButton}
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+
+
+ );
+ }
+ }
+
+ if (sessionMessages.length === 0 && !streamingMessageId) {
+ return (
+
+ {returnToParentButton}
+ {!isDesktopExpandedInput ? (
+
+
+
+ ) : null}
+
+
+
+
+ );
+ }
+
+ return (
+
+ {returnToParentButton}
+
+
+
+
+ 0}
+ onRenderEarlier={handleRenderEarlier}
+ scrollToBottom={scrollToBottom}
+ scrollRef={scrollRef}
+ />
+
+
+
+
+
+
+
+ {!isDesktopExpandedInput && showScrollButton && sessionMessages.length > 0 && (
+
+ scrollToBottom({ force: true })}
+ className="rounded-full h-8 w-8 p-0 shadow-none bg-background/95 hover:bg-interactive-hover"
+ aria-label="Scroll to bottom"
+ >
+
+
+
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/ui/src/components/chat/ChatEmptyState.tsx b/ui/src/components/chat/ChatEmptyState.tsx
new file mode 100644
index 0000000..0dbd93f
--- /dev/null
+++ b/ui/src/components/chat/ChatEmptyState.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { RiGitBranchLine } from '@remixicon/react';
+
+import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
+import { TextLoop } from '@/components/ui/TextLoop';
+import { useThemeSystem } from '@/contexts/useThemeSystem';
+import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
+import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
+import { useGitStatus, useGitStore } from '@/stores/useGitStore';
+
+const phrases = [
+ "Fix the failing tests",
+ "Refactor this to be more readable",
+ "Add form validation",
+ "Optimize this function",
+ "Write tests for this",
+ "Explain how this works",
+ "Add a new feature",
+ "Help me debug this",
+ "Review my code",
+ "Simplify this logic",
+ "Add error handling",
+ "Create a new component",
+ "Update the documentation",
+ "Find the bug here",
+ "Improve performance",
+ "Add type definitions",
+];
+
+interface ChatEmptyStateProps {
+ showDraftContext?: boolean;
+}
+
+const ChatEmptyState: React.FC = ({
+ showDraftContext = false,
+}) => {
+ const { currentTheme } = useThemeSystem();
+ const { git } = useRuntimeAPIs();
+ const effectiveDirectory = useEffectiveDirectory();
+ const { setActiveDirectory, fetchStatus } = useGitStore();
+ const gitStatus = useGitStatus(effectiveDirectory ?? null);
+
+ // Use theme's muted foreground for secondary text
+ const textColor = currentTheme?.colors?.surface?.mutedForeground || 'var(--muted-foreground)';
+ const branchName = typeof gitStatus?.current === 'string' && gitStatus.current.trim().length > 0
+ ? gitStatus.current.trim()
+ : null;
+
+ React.useEffect(() => {
+ if (!showDraftContext || !effectiveDirectory) {
+ return;
+ }
+
+ setActiveDirectory(effectiveDirectory);
+
+ const state = useGitStore.getState().directories.get(effectiveDirectory);
+ if (!state?.status && state?.isGitRepo !== false) {
+ void fetchStatus(effectiveDirectory, git, { silent: true });
+ }
+ }, [effectiveDirectory, fetchStatus, git, setActiveDirectory, showDraftContext]);
+
+ return (
+
+
+ {showDraftContext && (
+
+ {branchName && (
+
+
+ {branchName}
+
+ )}
+
+ )}
+
+ {phrases.map((phrase) => (
+ "{phrase}…"
+ ))}
+
+
+ );
+};
+
+export default React.memo(ChatEmptyState);
diff --git a/ui/src/components/chat/ChatErrorBoundary.tsx b/ui/src/components/chat/ChatErrorBoundary.tsx
new file mode 100644
index 0000000..c2fc26a
--- /dev/null
+++ b/ui/src/components/chat/ChatErrorBoundary.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { RiChat3Line, RiRestartLine } from '@remixicon/react';
+import { Button } from '../ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
+
+interface ChatErrorBoundaryState {
+ hasError: boolean;
+ error?: Error;
+ errorInfo?: React.ErrorInfo;
+}
+
+interface ChatErrorBoundaryProps {
+ children: React.ReactNode;
+ sessionId?: string;
+}
+
+export class ChatErrorBoundary extends React.Component {
+ constructor(props: ChatErrorBoundaryProps) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(error: Error): ChatErrorBoundaryState {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
+ this.setState({ error, errorInfo });
+
+ if (process.env.NODE_ENV === 'development') {
+ console.error('Chat error caught by boundary:', error, errorInfo);
+ }
+ }
+
+ handleReset = () => {
+ this.setState({ hasError: false, error: undefined, errorInfo: undefined });
+ };
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+
+
+
+ Chat Error
+
+
+
+
+ The chat interface encountered an error. This might be due to a temporary network issue or corrupted message data.
+
+
+ {this.props.sessionId && (
+
+ Session: {this.props.sessionId}
+
+ )}
+
+ {this.state.error && (
+
+ Error details
+
+ {this.state.error.toString()}
+
+
+ )}
+
+
+
+
+ Reset Chat
+
+
+
+
+ If the problem persists, try refreshing the page.
+
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/ui/src/components/chat/ChatInput.tsx b/ui/src/components/chat/ChatInput.tsx
new file mode 100644
index 0000000..b6a18b7
--- /dev/null
+++ b/ui/src/components/chat/ChatInput.tsx
@@ -0,0 +1,2822 @@
+import React from 'react';
+import { Textarea } from '@/components/ui/textarea';
+import {
+ RiAddCircleLine,
+ RiAiAgentLine,
+ RiAttachment2,
+ RiCloseLine,
+ RiCommandLine,
+ RiExternalLinkLine,
+ RiFullscreenLine,
+ RiGitPullRequestLine,
+ RiGithubLine,
+ RiSendPlane2Line,
+} from '@remixicon/react';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore';
+import type { AttachedFile } from '@/stores/types/sessionTypes';
+import { useInlineCommentDraftStore, type InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
+import { appendInlineComments } from '@/lib/messages/inlineComments';
+import { AttachedFilesList } from './FileAttachment';
+import { QueuedMessageChips } from './QueuedMessageChips';
+import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete';
+import { CommandAutocomplete, type CommandAutocompleteHandle } from './CommandAutocomplete';
+import { SkillAutocomplete, type SkillAutocompleteHandle } from './SkillAutocomplete';
+import { cn, isMacOS } from '@/lib/utils';
+import { ModelControls } from './ModelControls';
+import { UnifiedControlsDrawer } from './UnifiedControlsDrawer';
+import { parseAgentMentions } from '@/lib/messages/agentMentions';
+import { StatusRow } from './StatusRow';
+import { MobileAgentButton } from './MobileAgentButton';
+import { MobileModelButton } from './MobileModelButton';
+import { MobileSessionStatusBar } from './MobileSessionStatusBar';
+import { useAssistantStatus } from '@/hooks/useAssistantStatus';
+import { useCurrentSessionActivity } from '@/hooks/useSessionActivity';
+import { toast } from '@/components/ui';
+import { useFileStore } from '@/stores/fileStore';
+import { useMessageStore } from '@/stores/messageStore';
+import { isTauriShell, isVSCodeRuntime } from '@/lib/desktop';
+import { isIMECompositionEvent } from '@/lib/ime';
+import { StopIcon } from '@/components/icons/StopIcon';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
+import type { MobileControlsPanel } from './mobileControlsUtils';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { useThemeSystem } from '@/contexts/useThemeSystem';
+import { GitHubIssuePickerDialog } from '@/components/session/GitHubIssuePickerDialog';
+import { GitHubPrPickerDialog } from '@/components/session/GitHubPrPickerDialog';
+import { useChatSearchDirectory } from '@/hooks/useChatSearchDirectory';
+import { opencodeClient } from '@/lib/opencode/client';
+
+const MAX_VISIBLE_TEXTAREA_LINES = 8;
+const EMPTY_QUEUE: QueuedMessage[] = [];
+const FILE_MENTION_TOKEN = /^@[^\s]+$/;
+
+interface ChatInputProps {
+ onOpenSettings?: () => void;
+ scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
+}
+
+type AutocompleteOverlayPosition = {
+ top: number;
+ left: number;
+ place: 'above' | 'below';
+ maxHeight: number;
+};
+
+// Per-session draft key — preserves in-progress messages across project switches
+const getDraftKey = (sessionId: string | null): string =>
+ `openchamber_chat_input_draft_${sessionId ?? 'new'}`;
+
+// Helper to safely read from localStorage for a given session
+const getStoredDraft = (sessionId: string | null): string => {
+ try {
+ return localStorage.getItem(getDraftKey(sessionId)) ?? '';
+ } catch {
+ return '';
+ }
+};
+
+// Helper to safely write/clear a per-session draft
+const saveStoredDraft = (sessionId: string | null, draft: string): void => {
+ try {
+ if (draft) {
+ localStorage.setItem(getDraftKey(sessionId), draft);
+ } else {
+ localStorage.removeItem(getDraftKey(sessionId));
+ }
+ } catch {
+ // Ignore localStorage errors
+ }
+};
+
+export const ChatInput: React.FC = ({ onOpenSettings, scrollToBottom }) => {
+ // Track if we restored a draft on mount (for text selection)
+ const initialDraftRef = React.useRef(null);
+ // Track initial session ID (captured at mount time for draft restoration)
+ const initialSessionIdRef = React.useRef(null);
+ const [message, setMessage] = React.useState(() => {
+ // Read per-session draft at mount time using the current session from the store
+ const sessionId = useSessionStore.getState().currentSessionId;
+ initialSessionIdRef.current = sessionId;
+ const draft = getStoredDraft(sessionId);
+ if (draft) {
+ initialDraftRef.current = draft;
+ }
+ return draft;
+ });
+ const [inputMode, setInputMode] = React.useState<'normal' | 'shell'>('normal');
+ const [isDragging, setIsDragging] = React.useState(false);
+ const [showFileMention, setShowFileMention] = React.useState(false);
+ const [mentionQuery, setMentionQuery] = React.useState('');
+ const [showCommandAutocomplete, setShowCommandAutocomplete] = React.useState(false);
+ const [commandQuery, setCommandQuery] = React.useState('');
+ const [autocompleteTab, setAutocompleteTab] = React.useState<'commands' | 'agents' | 'files'>('commands');
+ const [showSkillAutocomplete, setShowSkillAutocomplete] = React.useState(false);
+ const [skillQuery, setSkillQuery] = React.useState('');
+ const [textareaSize, setTextareaSize] = React.useState<{ height: number; maxHeight: number } | null>(null);
+ const [mobileControlsOpen, setMobileControlsOpen] = React.useState(false);
+ const [mobileControlsPanel, setMobileControlsPanel] = React.useState(null);
+ // Message history navigation state (up/down arrow to recall previous messages)
+ const [historyIndex, setHistoryIndex] = React.useState(-1); // -1 = not browsing, 0+ = index from most recent
+ const [draftMessage, setDraftMessage] = React.useState(''); // Preserves input when entering history mode
+ const textareaRef = React.useRef(null);
+ const dropZoneRef = React.useRef(null);
+ const canAcceptDropRef = React.useRef(false);
+ const nativeDragInsideDropZoneRef = React.useRef(false);
+ const mentionRef = React.useRef(null);
+ const commandRef = React.useRef(null);
+ const skillRef = React.useRef(null);
+ // Ref to track current message value without triggering re-renders in effects
+ const messageRef = React.useRef(message);
+
+ const sendMessage = useSessionStore((state) => state.sendMessage);
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+ const newSessionDraftOpen = useSessionStore((state) => state.newSessionDraft?.open);
+ const abortCurrentOperation = useSessionStore((state) => state.abortCurrentOperation);
+ const acknowledgeSessionAbort = useSessionStore((state) => state.acknowledgeSessionAbort);
+ const abortPromptSessionId = useSessionStore((state) => state.abortPromptSessionId);
+ const clearAbortPrompt = useSessionStore((state) => state.clearAbortPrompt);
+ const attachedFiles = useSessionStore((state) => state.attachedFiles);
+ const addAttachedFile = useSessionStore((state) => state.addAttachedFile);
+ const clearAttachedFiles = useSessionStore((state) => state.clearAttachedFiles);
+ const saveSessionAgentSelection = useSessionStore((state) => state.saveSessionAgentSelection);
+ const consumePendingInputText = useSessionStore((state) => state.consumePendingInputText);
+ const pendingInputText = useSessionStore((state) => state.pendingInputText);
+ const consumePendingSyntheticParts = useSessionStore((state) => state.consumePendingSyntheticParts);
+
+ const { currentProviderId, currentModelId, currentVariant, currentAgentName, setAgent, getVisibleAgents } = useConfigStore();
+ const agents = getVisibleAgents();
+ const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]);
+ const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen, cornerRadius, persistChatDraft, inputSpellcheckEnabled, isExpandedInput, setExpandedInput } = useUIStore();
+ const { working } = useAssistantStatus();
+ const { currentTheme } = useThemeSystem();
+ const chatSearchDirectory = useChatSearchDirectory();
+ const [showAbortStatus, setShowAbortStatus] = React.useState(false);
+ const [textareaScrollTop, setTextareaScrollTop] = React.useState(0);
+
+ const isDesktopExpanded = isExpandedInput && !isMobile;
+
+ const sendableAttachedFiles = React.useMemo(
+ () => attachedFiles.filter((file) => file.source !== 'server'),
+ [attachedFiles],
+ );
+
+ const hasInlineMentionForHighlight = React.useMemo(() => {
+ if (!message || !message.includes('@') || inputMode === 'shell') {
+ return false;
+ }
+ const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase()));
+ const mentionRegex = /@([^\s]+)/g;
+ let match: RegExpExecArray | null;
+ while ((match = mentionRegex.exec(message)) !== null) {
+ const offset = match.index;
+ const charBefore = offset > 0 ? message[offset - 1] : null;
+ if (charBefore && !/(\s|\(|\)|\[|\]|\{|\}|"|'|`|,|\.|;|:)/.test(charBefore)) {
+ continue;
+ }
+ const mentionPath = String(match[1] || '').trim().replace(/[),.;:!?`"'>]+$/g, '');
+ if (!mentionPath) {
+ continue;
+ }
+ if (knownAgentNames.has(mentionPath.toLowerCase())) {
+ return true;
+ }
+ if (mentionPath.includes('/') || mentionPath.includes('\\') || mentionPath.includes('.')) {
+ return true;
+ }
+ }
+ return false;
+ }, [agents, inputMode, message]);
+
+ const highlightedComposerContent = React.useMemo(() => {
+ if (!hasInlineMentionForHighlight) {
+ return null;
+ }
+
+ const parts: Array<{ text: string; mentionKind: 'none' | 'file' | 'agent' }> = [];
+ const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase()));
+ const mentionRegex = /@([^\s]+)/g;
+ let lastIndex = 0;
+ let match: RegExpExecArray | null;
+
+ while ((match = mentionRegex.exec(message)) !== null) {
+ const full = match[0];
+ const mention = String(match[1] || '').trim().replace(/[),.;:!?`"'>]+$/g, '');
+ const start = match.index;
+ const end = start + full.length;
+ const charBefore = start > 0 ? message[start - 1] : null;
+ const isBoundary = !charBefore || /(\s|\(|\)|\[|\]|\{|\}|"|'|`|,|\.|;|:)/.test(charBefore);
+ const isAgentMention = isBoundary && mention.length > 0 && knownAgentNames.has(mention.toLowerCase());
+ const isFileMention = isBoundary
+ && mention.length > 0
+ && !knownAgentNames.has(mention.toLowerCase())
+ && (mention.includes('/') || mention.includes('\\') || mention.includes('.'));
+
+ if (start > lastIndex) {
+ parts.push({ text: message.slice(lastIndex, start), mentionKind: 'none' });
+ }
+ parts.push({
+ text: full,
+ mentionKind: isFileMention ? 'file' : isAgentMention ? 'agent' : 'none',
+ });
+ lastIndex = end;
+ }
+
+ if (lastIndex < message.length) {
+ parts.push({ text: message.slice(lastIndex), mentionKind: 'none' });
+ }
+
+ return parts;
+ }, [agents, hasInlineMentionForHighlight, message]);
+
+ const sanitizeAttachmentsForSend = React.useCallback(
+ (files: AttachedFile[] | undefined): AttachedFile[] => (files ?? [])
+ .filter((file) => file.source !== 'server')
+ .map((file) => ({ ...file })),
+ [],
+ );
+
+ const extractInlineFileMentions = React.useCallback((rawText: string): { sanitizedText: string; attachments: AttachedFile[] } => {
+ if (!rawText || !rawText.includes('@')) {
+ return { sanitizedText: rawText, attachments: [] };
+ }
+
+ const clientDirectory = opencodeClient.getDirectory() || '';
+ const root = (chatSearchDirectory || clientDirectory).replace(/\\/g, '/').replace(/\/+$/, '');
+ const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase()));
+ const seenPaths = new Set();
+ const attachments: AttachedFile[] = [];
+
+ const mentionRegex = /@([^\s]+)/g;
+ let match: RegExpExecArray | null;
+ while ((match = mentionRegex.exec(rawText)) !== null) {
+ const rawMentionPath = match[1];
+ const offset = match.index;
+ const original = rawText;
+ const charBefore = offset > 0 ? original[offset - 1] : null;
+ if (charBefore && !/(\s|\(|\)|\[|\]|\{|\}|"|'|`|,|\.|;|:)/.test(charBefore)) {
+ continue;
+ }
+
+ const mentionPath = String(rawMentionPath || '')
+ .trim()
+ .replace(/^[`"'<(]+/, '')
+ .replace(/[),.;:!?`"'>]+$/g, '');
+ if (!mentionPath) {
+ continue;
+ }
+
+ if (knownAgentNames.has(mentionPath.toLowerCase())) {
+ continue;
+ }
+
+ const looksLikeFilePath = mentionPath.includes('/') || mentionPath.includes('\\') || mentionPath.includes('.');
+ if (!looksLikeFilePath) {
+ continue;
+ }
+
+ const normalizedMentionPath = mentionPath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '');
+ if (!normalizedMentionPath) {
+ continue;
+ }
+
+ const serverPath = mentionPath.startsWith('/')
+ ? mentionPath.replace(/\\/g, '/')
+ : root
+ ? `${root}/${normalizedMentionPath}`
+ : null;
+
+ if (!serverPath) {
+ continue;
+ }
+
+ const normalizedServerPath = serverPath.replace(/\/+/g, '/');
+ if (seenPaths.has(normalizedServerPath)) {
+ continue;
+ }
+ seenPaths.add(normalizedServerPath);
+
+ const filename = normalizedMentionPath.split('/').filter(Boolean).pop() || normalizedMentionPath;
+ attachments.push({
+ id: `inline-server-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
+ file: new File([], filename, { type: 'text/plain' }),
+ filename,
+ mimeType: 'text/plain',
+ size: 0,
+ dataUrl: normalizedServerPath,
+ source: 'server',
+ serverPath: normalizedServerPath,
+ });
+ }
+
+ return {
+ sanitizedText: rawText,
+ attachments,
+ };
+ }, [agents, chatSearchDirectory]);
+ const [autocompleteOverlayPosition, setAutocompleteOverlayPosition] = React.useState(null);
+ const abortTimeoutRef = React.useRef | null>(null);
+ const prevWasAbortedRef = React.useRef(false);
+
+ // Issue linking state
+ const [issuePickerOpen, setIssuePickerOpen] = React.useState(false);
+ const [prPickerOpen, setPrPickerOpen] = React.useState(false);
+ const [linkedIssue, setLinkedIssue] = React.useState<{
+ number: number;
+ title: string;
+ url: string;
+ contextText: string;
+ author?: { login: string; avatarUrl?: string };
+ } | null>(null);
+ const [linkedPr, setLinkedPr] = React.useState<{
+ number: number;
+ title: string;
+ url: string;
+ head: string;
+ base: string;
+ includeDiff: boolean;
+ instructionsText: string;
+ contextText: string;
+ author?: { login: string; avatarUrl?: string };
+ } | null>(null);
+
+ // Message queue
+ const queueModeEnabled = useMessageQueueStore((state) => state.queueModeEnabled);
+ const queuedMessages = useMessageQueueStore(
+ React.useCallback(
+ (state) => {
+ if (!currentSessionId) return EMPTY_QUEUE;
+ return state.queuedMessages[currentSessionId] ?? EMPTY_QUEUE;
+ },
+ [currentSessionId]
+ )
+ );
+ const addToQueue = useMessageQueueStore((state) => state.addToQueue);
+ const clearQueue = useMessageQueueStore((state) => state.clearQueue);
+
+ // Inline comment drafts
+ const draftCount = useInlineCommentDraftStore(
+ React.useCallback(
+ (state) => {
+ const sessionKey = currentSessionId ?? (newSessionDraftOpen ? 'draft' : '');
+ if (!sessionKey) return 0;
+ return (state.drafts[sessionKey] ?? []).length;
+ },
+ [currentSessionId, newSessionDraftOpen]
+ )
+ );
+ const consumeDrafts = useInlineCommentDraftStore((state) => state.consumeDrafts);
+ const hasDrafts = draftCount > 0;
+
+ // User message history for up/down arrow navigation
+ // Get raw messages from store (stable reference)
+ const sessionMessages = useMessageStore(
+ React.useCallback(
+ (state) => (currentSessionId ? state.messages.get(currentSessionId) : undefined),
+ [currentSessionId]
+ )
+ );
+ // Derive user message history with useMemo to avoid infinite re-renders
+ const userMessageHistory = React.useMemo(() => {
+ if (!sessionMessages) return [];
+ return sessionMessages
+ .filter((m) => m.info.role === 'user')
+ .map((m) => {
+ const textPart = m.parts.find((p) => p.type === 'text');
+ if (textPart && 'text' in textPart) {
+ return String(textPart.text);
+ }
+ return '';
+ })
+ .filter((text) => text.length > 0)
+ .reverse(); // Most recent first
+ }, [sessionMessages]);
+
+ // Keep messageRef in sync with message state
+ React.useEffect(() => {
+ messageRef.current = message;
+ }, [message]);
+
+ // Handle initial draft restoration and text selection
+ const hasHandledInitialDraftRef = React.useRef(false);
+ React.useEffect(() => {
+ if (hasHandledInitialDraftRef.current) return;
+ hasHandledInitialDraftRef.current = true;
+
+ const draft = initialDraftRef.current;
+ if (!draft) return;
+
+ if (!persistChatDraft) {
+ // Setting disabled - clear the restored draft
+ setMessage('');
+ try {
+ localStorage.removeItem(getDraftKey(initialSessionIdRef.current));
+ } catch {
+ // Ignore
+ }
+ } else {
+ // Setting enabled - select all text
+ requestAnimationFrame(() => {
+ textareaRef.current?.select();
+ });
+ }
+ }, [persistChatDraft]);
+
+ // Handle session switching: save draft for old session, restore draft for new session
+ const prevSessionIdRef = React.useRef(currentSessionId);
+ React.useEffect(() => {
+ if (prevSessionIdRef.current !== currentSessionId) {
+ const oldSessionId = prevSessionIdRef.current;
+ prevSessionIdRef.current = currentSessionId;
+ setInputMode('normal');
+
+ if (persistChatDraft) {
+ // Save current draft for the session we're leaving
+ saveStoredDraft(oldSessionId, messageRef.current);
+ // Restore draft for the session we're entering
+ const newDraft = getStoredDraft(currentSessionId);
+ setMessage(newDraft);
+ if (newDraft) {
+ requestAnimationFrame(() => {
+ textareaRef.current?.select();
+ });
+ }
+ } else {
+ // Persist disabled: clear input without saving
+ setMessage('');
+ }
+ }
+ }, [currentSessionId, persistChatDraft]);
+
+ // Focus textarea when new session draft is opened
+ const prevNewSessionDraftOpenRef = React.useRef(newSessionDraftOpen);
+ React.useEffect(() => {
+ if (!prevNewSessionDraftOpenRef.current && newSessionDraftOpen) {
+ // New session draft just opened - focus the textarea
+ requestAnimationFrame(() => {
+ if (isMobile) {
+ // On mobile, use preventScroll to avoid viewport jumping
+ textareaRef.current?.focus({ preventScroll: true });
+ } else {
+ textareaRef.current?.focus();
+ }
+ });
+ }
+ prevNewSessionDraftOpenRef.current = newSessionDraftOpen;
+ }, [newSessionDraftOpen, isMobile]);
+
+ // Persist chat input draft to localStorage per session (only if setting enabled)
+ React.useEffect(() => {
+ if (!persistChatDraft) {
+ // Clear stored draft for current session when setting is disabled
+ try {
+ localStorage.removeItem(getDraftKey(currentSessionId));
+ } catch {
+ // Ignore
+ }
+ return;
+ }
+ saveStoredDraft(currentSessionId, message);
+ }, [message, persistChatDraft, currentSessionId]);
+
+ // Session activity for queue availability and controls
+ const { phase: sessionPhase } = useCurrentSessionActivity();
+
+ const handleTextareaPointerDownCapture = React.useCallback((event: React.PointerEvent) => {
+ if (!isMobile) {
+ return;
+ }
+
+ if (event.pointerType !== 'touch') {
+ return;
+ }
+
+ const textarea = textareaRef.current;
+ if (!textarea) {
+ return;
+ }
+
+ if (document.activeElement === textarea) {
+ return;
+ }
+
+ // Prevent iOS from scrolling the page to reveal the input.
+ event.preventDefault();
+ event.stopPropagation();
+
+ const scroller = document.scrollingElement;
+ if (scroller && scroller.scrollTop !== 0) {
+ scroller.scrollTop = 0;
+ }
+ if (window.scrollY !== 0) {
+ window.scrollTo(0, 0);
+ }
+
+ try {
+ textarea.focus({ preventScroll: true });
+ } catch {
+ textarea.focus();
+ }
+
+ const len = textarea.value.length;
+ try {
+ textarea.setSelectionRange(len, len);
+ } catch {
+ // ignored
+ }
+ }, [isMobile]);
+
+ const handleOpenMobileControls = React.useCallback(() => {
+ if (!isMobile) {
+ return;
+ }
+
+ if (mobileControlsOpen) {
+ setMobileControlsOpen(false);
+ return;
+ }
+
+ setMobileControlsPanel(null);
+
+ if (isKeyboardOpen) {
+ textareaRef.current?.blur();
+ requestAnimationFrame(() => {
+ setMobileControlsOpen(true);
+ });
+ return;
+ }
+
+ setMobileControlsOpen(true);
+ }, [isMobile, isKeyboardOpen, mobileControlsOpen]);
+
+ const handleCloseMobileControls = React.useCallback(() => {
+ setMobileControlsOpen(false);
+ }, []);
+
+ const handleOpenMobilePanel = React.useCallback((panel: MobileControlsPanel) => {
+ if (!isMobile) {
+ return;
+ }
+ setMobileControlsOpen(false);
+ textareaRef.current?.blur();
+ requestAnimationFrame(() => {
+ setMobileControlsPanel(panel);
+ });
+ }, [isMobile]);
+
+ const handleReturnToUnifiedControls = React.useCallback(() => {
+ if (!isMobile) {
+ return;
+ }
+ setMobileControlsPanel(null);
+ requestAnimationFrame(() => {
+ setMobileControlsOpen(true);
+ });
+ }, [isMobile]);
+
+ // Consume pending input text (e.g., from revert action)
+ React.useEffect(() => {
+ if (pendingInputText !== null) {
+ const pending = consumePendingInputText();
+ if (pending?.text) {
+ if (pending.mode === 'append') {
+ setMessage((prev) => {
+ const next = pending.text.trim();
+ if (!next) return prev;
+ const base = prev.trimEnd();
+ if (!base.trim()) return next;
+ return `${base} ${next}`;
+ });
+ } else {
+ setMessage(pending.text);
+ }
+ // Focus textarea after setting message
+ setTimeout(() => {
+ textareaRef.current?.focus();
+ }, 0);
+ }
+ }
+ }, [pendingInputText, consumePendingInputText]);
+
+ const hasContent = message.trim() || sendableAttachedFiles.length > 0 || hasDrafts;
+ const hasQueuedMessages = queuedMessages.length > 0;
+ const canSend = hasContent || hasQueuedMessages;
+
+ const canAbort = working.isWorking;
+
+ // Keep a ref to handleSubmit so callbacks don't depend on it.
+ type SubmitOptions = {
+ queuedOnly?: boolean;
+ };
+ const handleSubmitRef = React.useRef<(options?: SubmitOptions) => Promise>(async () => {});
+
+ // Add message to queue instead of sending
+ const handleQueueMessage = React.useCallback(() => {
+ if (!hasContent || !currentSessionId) return;
+
+ const drafts = consumeDrafts(currentSessionId);
+
+ let messageToQueue = message.replace(/^\n+|\n+$/g, '');
+ if (drafts.length > 0) {
+ messageToQueue = appendInlineComments(messageToQueue, drafts);
+ }
+ const attachmentsToQueue = sanitizeAttachmentsForSend(sendableAttachedFiles);
+
+ addToQueue(currentSessionId, {
+ content: messageToQueue,
+ attachments: attachmentsToQueue.length > 0 ? attachmentsToQueue : undefined,
+ });
+
+ // Clear input and attachments
+ setMessage('');
+ if (attachmentsToQueue.length > 0) {
+ clearAttachedFiles();
+ }
+
+ if (!isMobile) {
+ textareaRef.current?.focus();
+ }
+ }, [hasContent, currentSessionId, message, sendableAttachedFiles, sanitizeAttachmentsForSend, addToQueue, clearAttachedFiles, isMobile, consumeDrafts]);
+
+ const handleSubmit = async (options?: SubmitOptions) => {
+ const queuedOnly = options?.queuedOnly ?? false;
+
+ if (queuedOnly) {
+ if (!hasQueuedMessages || !currentSessionId) return;
+ } else if (!canSend || (!currentSessionId && !newSessionDraftOpen)) {
+ return;
+ }
+
+ // Re-pin and scroll to bottom when sending
+ scrollToBottom?.({ instant: true, force: true });
+
+ if (!currentProviderId || !currentModelId) {
+ console.warn('Cannot send message: provider or model not selected');
+ return;
+ }
+
+ // Build the primary message (first part) and additional parts
+ let primaryText = '';
+ let primaryAttachments: AttachedFile[] = [];
+ let agentMentionName: string | undefined;
+ const additionalParts: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }> = [];
+
+ // Consume any pending synthetic parts (from conflict resolution, etc.)
+ const syntheticParts = consumePendingSyntheticParts();
+
+ // Process queued messages first
+ for (let i = 0; i < queuedMessages.length; i++) {
+ const queuedMsg = queuedMessages[i];
+ const { sanitizedText, mention } = parseAgentMentions(queuedMsg.content, agents);
+ const { sanitizedText: queuedText, attachments: mentionAttachments } = extractInlineFileMentions(sanitizedText);
+
+ // Use agent mention from first message that has one
+ if (!agentMentionName && mention?.name) {
+ agentMentionName = mention.name;
+ }
+
+ if (i === 0) {
+ // First queued message becomes primary
+ primaryText = queuedText;
+ primaryAttachments = [
+ ...sanitizeAttachmentsForSend(queuedMsg.attachments),
+ ...mentionAttachments,
+ ];
+ } else {
+ // Subsequent queued messages become additional parts
+ const queuedAttachments = sanitizeAttachmentsForSend(queuedMsg.attachments);
+ additionalParts.push({
+ text: queuedText,
+ attachments: [...queuedAttachments, ...mentionAttachments],
+ });
+ }
+ }
+
+ // Add current input (skip for queued-only auto-send)
+ if (!queuedOnly && hasContent) {
+ const messageToSend = message.replace(/^\n+|\n+$/g, '');
+ const { sanitizedText, mention } = parseAgentMentions(messageToSend, agents);
+ const { sanitizedText: messageText, attachments: mentionAttachments } = extractInlineFileMentions(sanitizedText);
+ const attachmentsToSend = sanitizeAttachmentsForSend(sendableAttachedFiles);
+
+ if (!agentMentionName && mention?.name) {
+ agentMentionName = mention.name;
+ }
+
+ if (queuedMessages.length === 0) {
+ // No queue - current input is primary
+ primaryText = messageText;
+ primaryAttachments = [...attachmentsToSend, ...mentionAttachments];
+ } else {
+ // Has queue - current input is additional part
+ additionalParts.push({
+ text: messageText,
+ attachments: [...attachmentsToSend, ...mentionAttachments],
+ });
+ }
+ }
+
+ const sessionKey = currentSessionId ?? (newSessionDraftOpen ? 'draft' : null);
+ let drafts: InlineCommentDraft[] = [];
+ if (!queuedOnly && sessionKey) {
+ drafts = consumeDrafts(sessionKey);
+ }
+
+ if (drafts.length > 0) {
+ if (queuedMessages.length === 0) {
+ primaryText = appendInlineComments(primaryText, drafts);
+ } else if (additionalParts.length > 0) {
+ const lastPart = additionalParts[additionalParts.length - 1];
+ lastPart.text = appendInlineComments(lastPart.text, drafts);
+ } else {
+ primaryText = appendInlineComments(primaryText, drafts);
+ }
+ }
+
+ // Add synthetic parts (from conflict resolution, etc.)
+ if (syntheticParts && syntheticParts.length > 0) {
+ for (const part of syntheticParts) {
+ additionalParts.push({
+ text: part.text,
+ synthetic: true,
+ });
+ }
+ }
+
+ // Add linked issue as synthetic part (only the parts with synthetic: true)
+ // The text part (synthetic: false) is completely dropped per requirements
+ if (linkedIssue) {
+ additionalParts.push({
+ text: linkedIssue.contextText,
+ synthetic: true,
+ });
+ }
+
+ if (linkedPr) {
+ additionalParts.push({
+ text: linkedPr.instructionsText,
+ synthetic: true,
+ });
+ additionalParts.push({
+ text: linkedPr.contextText,
+ synthetic: true,
+ });
+ }
+
+ if (!primaryText && additionalParts.length === 0) return;
+
+ // Clear queue and input
+ if (currentSessionId && hasQueuedMessages) {
+ clearQueue(currentSessionId);
+ }
+ if (!queuedOnly) {
+ setMessage('');
+ // Clear per-session draft on submit
+ saveStoredDraft(currentSessionId, '');
+ // Reset message history navigation state
+ setHistoryIndex(-1);
+ setDraftMessage('');
+ if (attachedFiles.length > 0) {
+ clearAttachedFiles();
+ }
+ // Close expanded input overlay when submitting
+ setExpandedInput(false);
+ }
+
+ if (isMobile) {
+ textareaRef.current?.blur();
+ }
+
+ // Handle local slash commands only in normal mode
+ const normalizedCommand = primaryText.trimStart();
+ if (inputMode === 'normal' && normalizedCommand.startsWith('/')) {
+ const commandName = normalizedCommand
+ .slice(1)
+ .trim()
+ .split(/\s+/)[0]
+ ?.toLowerCase();
+
+ // NEW: /undo - revert to last message (populates input with reverted message text)
+ if (commandName === 'undo' && currentSessionId) {
+ await useSessionStore.getState().handleSlashUndo(currentSessionId);
+ // Don't clear message - pendingInputText will populate it with reverted message
+ scrollToBottom?.({ instant: true, force: true });
+ return; // Don't send to assistant
+ }
+ // NEW: /redo - unrevert or partial redo (populates input with message text)
+ else if (commandName === 'redo' && currentSessionId) {
+ await useSessionStore.getState().handleSlashRedo(currentSessionId);
+ // Don't clear message - pendingInputText will populate it
+ scrollToBottom?.({ instant: true, force: true });
+ return; // Don't send to assistant
+ }
+ // NEW: /timeline - open timeline dialog
+ else if (commandName === 'timeline' && currentSessionId) {
+ setTimelineDialogOpen(true);
+ setMessage('');
+ return; // Don't send to assistant
+ }
+ }
+
+ // Collect all attachments for error recovery
+ const allAttachments = [
+ ...primaryAttachments,
+ ...additionalParts.flatMap(p => p.attachments ?? []),
+ ];
+
+ void sendMessage(
+ primaryText,
+ currentProviderId,
+ currentModelId,
+ currentAgentName,
+ primaryAttachments,
+ agentMentionName,
+ additionalParts.length > 0 ? additionalParts : undefined,
+ currentVariant,
+ inputMode
+ ).then(() => {
+ // Clear linked issue after successful message send
+ if (linkedIssue) {
+ setLinkedIssue(null);
+ }
+ if (linkedPr) {
+ setLinkedPr(null);
+ }
+ }).catch((error: unknown) => {
+ const rawMessage =
+ error instanceof Error
+ ? error.message
+ : typeof error === 'string'
+ ? error
+ : String(error ?? '');
+ const normalized = rawMessage.toLowerCase();
+
+ console.error('Message send failed:', rawMessage || error);
+
+ const isSoftNetworkError =
+ normalized.includes('timeout') ||
+ normalized.includes('timed out') ||
+ normalized.includes('may still be processing') ||
+ normalized.includes('being processed') ||
+ normalized.includes('failed to fetch') ||
+ normalized.includes('networkerror') ||
+ normalized.includes('network error') ||
+ normalized.includes('gateway timeout') ||
+ normalized === 'failed to send message';
+
+ if (normalized.includes('payload too large') || normalized.includes('413') || normalized.includes('entity too large')) {
+ toast.error('Attachments are too large to send. Please try reducing the number or size of images.');
+ if (allAttachments.length > 0) {
+ useFileStore.setState({ attachedFiles: allAttachments });
+ }
+ return;
+ }
+
+ if (isSoftNetworkError) {
+ if (allAttachments.length > 0) {
+ useFileStore.setState({ attachedFiles: allAttachments });
+ toast.error('Failed to send attachments. Try fewer files or smaller images.');
+ }
+ return;
+ }
+
+ if (allAttachments.length > 0) {
+ useFileStore.setState({ attachedFiles: allAttachments });
+ }
+ toast.error(rawMessage || 'Message failed to send. Attachments restored.');
+ });
+
+ if (!isMobile) {
+ textareaRef.current?.focus();
+ }
+ };
+
+ // Update ref with latest handleSubmit on every render
+ handleSubmitRef.current = handleSubmit;
+
+ // Primary action for send button - respects queue mode setting
+ const handlePrimaryAction = React.useCallback(() => {
+ const canQueue = inputMode === 'normal' && hasContent && currentSessionId && sessionPhase !== 'idle';
+ if (queueModeEnabled && canQueue) {
+ handleQueueMessage();
+ } else {
+ void handleSubmitRef.current();
+ }
+ }, [inputMode, hasContent, currentSessionId, sessionPhase, queueModeEnabled, handleQueueMessage]);
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ // Early return during IME composition to prevent interference with autocomplete.
+ // Uses keyCode === 229 fallback for WebKit where compositionend fires before keydown.
+ if (isIMECompositionEvent(e)) return;
+
+ if (inputMode === 'shell' && e.key === 'Escape') {
+ e.preventDefault();
+ setInputMode('normal');
+ return;
+ }
+
+ if (inputMode === 'shell' && e.key === 'Backspace' && message.length === 0) {
+ e.preventDefault();
+ setInputMode('normal');
+ return;
+ }
+
+ if ((e.key === 'Backspace' || e.key === 'Delete') && !e.metaKey && !e.ctrlKey && !e.altKey) {
+ const textarea = textareaRef.current;
+ const selectionStart = textarea?.selectionStart ?? message.length;
+ const selectionEnd = textarea?.selectionEnd ?? message.length;
+ const hasCollapsedSelection = selectionStart === selectionEnd;
+
+ if (hasCollapsedSelection) {
+ const probeIndex = e.key === 'Backspace' ? selectionStart - 1 : selectionStart;
+ if (probeIndex >= 0 && probeIndex < message.length) {
+ let tokenStart = probeIndex;
+ while (tokenStart > 0 && !/\s/.test(message[tokenStart - 1])) {
+ tokenStart -= 1;
+ }
+
+ let tokenEnd = probeIndex + 1;
+ while (tokenEnd < message.length && !/\s/.test(message[tokenEnd])) {
+ tokenEnd += 1;
+ }
+
+ const token = message.slice(tokenStart, tokenEnd);
+ const looksLikeFileMention = FILE_MENTION_TOKEN.test(token)
+ && (token.includes('/') || token.includes('\\') || token.includes('.'));
+
+ if (looksLikeFileMention) {
+ const removeUntil = message[tokenEnd] === ' ' ? tokenEnd + 1 : tokenEnd;
+ const nextMessage = `${message.slice(0, tokenStart)}${message.slice(removeUntil)}`;
+ e.preventDefault();
+ setMessage(nextMessage);
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = tokenStart;
+ textareaRef.current.selectionEnd = tokenStart;
+ }
+ adjustTextareaHeight();
+ });
+ updateAutocompleteState(nextMessage, tokenStart);
+ return;
+ }
+ }
+ }
+ }
+
+ if (showCommandAutocomplete && commandRef.current) {
+ if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') {
+ e.preventDefault();
+ commandRef.current.handleKeyDown(e.key);
+ return;
+ }
+ }
+
+ if (showSkillAutocomplete && skillRef.current) {
+ if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') {
+ e.preventDefault();
+ skillRef.current.handleKeyDown(e.key);
+ return;
+ }
+ }
+
+ if (showFileMention && mentionRef.current) {
+ if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') {
+ e.preventDefault();
+ mentionRef.current.handleKeyDown(e.key);
+ return;
+ }
+ }
+
+ if (isDesktopExpanded && e.key === 'Escape') {
+ e.preventDefault();
+ setExpandedInput(false);
+ return;
+ }
+
+ if (e.key === 'Tab' && !showCommandAutocomplete && !showFileMention) {
+ e.preventDefault();
+ handleCycleAgent();
+ return;
+ }
+
+ // Handle ArrowUp/ArrowDown for message history navigation
+ // ArrowUp: only when cursor at start (position 0) or input is empty
+ // ArrowDown: also works when cursor at end (to cycle forward through history)
+ const isAnyAutocompleteOpen = showCommandAutocomplete || showSkillAutocomplete || showFileMention;
+ const cursorAtStart = textareaRef.current?.selectionStart === 0 && textareaRef.current?.selectionEnd === 0;
+ const cursorAtEnd = textareaRef.current?.selectionStart === message.length && textareaRef.current?.selectionEnd === message.length;
+ const canNavigateHistoryUp = !isAnyAutocompleteOpen && (message.length === 0 || cursorAtStart);
+ const canNavigateHistoryDown = !isAnyAutocompleteOpen && (message.length === 0 || cursorAtEnd);
+
+ if (e.key === 'ArrowUp' && canNavigateHistoryUp && userMessageHistory.length > 0) {
+ e.preventDefault();
+ if (historyIndex === -1) {
+ // Entering history mode - save current input as draft
+ setDraftMessage(message);
+ setHistoryIndex(0);
+ setMessage(userMessageHistory[0]);
+ } else if (historyIndex < userMessageHistory.length - 1) {
+ // Navigate to older message
+ const newIndex = historyIndex + 1;
+ setHistoryIndex(newIndex);
+ setMessage(userMessageHistory[newIndex]);
+ }
+ // Move cursor to start after history navigation
+ requestAnimationFrame(() => {
+ textareaRef.current?.setSelectionRange(0, 0);
+ });
+ // If at oldest message, do nothing
+ return;
+ }
+
+ if (e.key === 'ArrowDown' && canNavigateHistoryDown && historyIndex >= 0) {
+ e.preventDefault();
+ if (historyIndex === 0) {
+ // Exit history mode - restore draft
+ setHistoryIndex(-1);
+ setMessage(draftMessage);
+ setDraftMessage('');
+ } else {
+ // Navigate to newer message
+ const newIndex = historyIndex - 1;
+ setHistoryIndex(newIndex);
+ setMessage(userMessageHistory[newIndex]);
+ }
+ return;
+ }
+
+ // Handle Enter/Ctrl+Enter based on queue mode
+ if (e.key === 'Enter' && !e.shiftKey && (!isMobile || e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+
+ const isCtrlEnter = e.ctrlKey || e.metaKey;
+
+ // Queue mode: Enter queues, Ctrl+Enter sends
+ // Normal mode: Enter sends, Ctrl+Enter queues
+ // Note: Queueing only works when there's an existing session (currentSessionId)
+ // For new sessions (draft), always send immediately
+ const canQueue = inputMode === 'normal' && hasContent && currentSessionId && sessionPhase !== 'idle';
+
+ if (queueModeEnabled) {
+ if (isCtrlEnter || !canQueue) {
+ // Ctrl+Enter sends, or Enter when can't queue (new session)
+ handleSubmit();
+ } else {
+ // Enter queues when we have a session
+ handleQueueMessage();
+ }
+ } else {
+ if (isCtrlEnter && canQueue) {
+ // Ctrl+Enter queues when we have a session
+ handleQueueMessage();
+ } else {
+ // Enter sends
+ handleSubmit();
+ }
+ }
+ }
+ };
+
+ const measureCaretInTextarea = React.useCallback((textarea: HTMLTextAreaElement, cursorPosition: number) => {
+ const doc = textarea.ownerDocument;
+ const win = doc.defaultView;
+ if (!win) return null;
+
+ const style = win.getComputedStyle(textarea);
+ const mirror = doc.createElement('div');
+ const mirrorStyle = mirror.style;
+
+ mirrorStyle.position = 'absolute';
+ mirrorStyle.visibility = 'hidden';
+ mirrorStyle.pointerEvents = 'none';
+ mirrorStyle.whiteSpace = 'pre-wrap';
+ mirrorStyle.wordWrap = 'break-word';
+ mirrorStyle.overflow = 'hidden';
+ mirrorStyle.left = '-9999px';
+ mirrorStyle.top = '0';
+
+ mirrorStyle.width = `${textarea.clientWidth}px`;
+ mirrorStyle.font = style.font;
+ mirrorStyle.fontSize = style.fontSize;
+ mirrorStyle.fontFamily = style.fontFamily;
+ mirrorStyle.fontWeight = style.fontWeight;
+ mirrorStyle.fontStyle = style.fontStyle;
+ mirrorStyle.fontVariant = style.fontVariant;
+ mirrorStyle.letterSpacing = style.letterSpacing;
+ mirrorStyle.textTransform = style.textTransform;
+ mirrorStyle.textIndent = style.textIndent;
+ mirrorStyle.padding = style.padding;
+ mirrorStyle.border = style.border;
+ mirrorStyle.boxSizing = style.boxSizing;
+ mirrorStyle.lineHeight = style.lineHeight;
+ mirrorStyle.tabSize = style.tabSize;
+
+ mirror.textContent = textarea.value.slice(0, cursorPosition);
+ const marker = doc.createElement('span');
+ marker.textContent = textarea.value.slice(cursorPosition, cursorPosition + 1) || ' ';
+ mirror.appendChild(marker);
+
+ doc.body.appendChild(mirror);
+ const top = marker.offsetTop;
+ const left = marker.offsetLeft;
+ doc.body.removeChild(mirror);
+
+ return { top, left };
+ }, []);
+
+ const updateAutocompleteOverlayPosition = React.useCallback(() => {
+ if (!isDesktopExpanded) {
+ setAutocompleteOverlayPosition(null);
+ return;
+ }
+
+ if (!showCommandAutocomplete && !showSkillAutocomplete && !showFileMention) {
+ setAutocompleteOverlayPosition(null);
+ return;
+ }
+
+ const textarea = textareaRef.current;
+ const container = dropZoneRef.current;
+ if (!textarea || !container) return;
+
+ const cursor = textarea.selectionStart ?? message.length;
+ const caret = measureCaretInTextarea(textarea, cursor);
+ if (!caret) return;
+
+ const textareaRect = textarea.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+
+ const caretY = textareaRect.top - containerRect.top + (caret.top - textarea.scrollTop);
+ const caretX = textareaRect.left - containerRect.left + (caret.left - textarea.scrollLeft);
+
+ const popupMargin = 8;
+ const estimatedPopupHeight = 260;
+ const spaceAbove = caretY - popupMargin;
+ const spaceBelow = containerRect.height - caretY - popupMargin;
+ const place: 'above' | 'below' = spaceBelow >= estimatedPopupHeight || spaceBelow >= spaceAbove ? 'below' : 'above';
+
+ const desiredWidth = showFileMention ? 520 : showCommandAutocomplete ? 450 : 360;
+ const clampedLeft = Math.max(
+ popupMargin,
+ Math.min(caretX - 24, containerRect.width - desiredWidth - popupMargin)
+ );
+
+ const maxHeight = Math.max(120, Math.min(estimatedPopupHeight, place === 'below' ? spaceBelow : spaceAbove));
+
+ setAutocompleteOverlayPosition({
+ top: place === 'below' ? caretY + 22 : caretY - 6,
+ left: clampedLeft,
+ place,
+ maxHeight,
+ });
+ }, [
+ isDesktopExpanded,
+ measureCaretInTextarea,
+ message.length,
+ showCommandAutocomplete,
+ showFileMention,
+ showSkillAutocomplete,
+ ]);
+
+ React.useLayoutEffect(() => {
+ updateAutocompleteOverlayPosition();
+ }, [
+ updateAutocompleteOverlayPosition,
+ message,
+ showCommandAutocomplete,
+ showSkillAutocomplete,
+ showFileMention,
+ isDesktopExpanded,
+ ]);
+
+ React.useEffect(() => {
+ if (!isDesktopExpanded) return;
+ const onResize = () => updateAutocompleteOverlayPosition();
+ window.addEventListener('resize', onResize);
+ return () => {
+ window.removeEventListener('resize', onResize);
+ };
+ }, [isDesktopExpanded, updateAutocompleteOverlayPosition]);
+
+ const startAbortIndicator = React.useCallback(() => {
+ if (abortTimeoutRef.current) {
+ clearTimeout(abortTimeoutRef.current);
+ abortTimeoutRef.current = null;
+ }
+
+ setShowAbortStatus(true);
+
+ abortTimeoutRef.current = setTimeout(() => {
+ setShowAbortStatus(false);
+ abortTimeoutRef.current = null;
+ }, 1800);
+ }, []);
+
+ const handleAbort = React.useCallback(() => {
+ clearAbortPrompt();
+ startAbortIndicator();
+
+ void abortCurrentOperation();
+ }, [abortCurrentOperation, clearAbortPrompt, startAbortIndicator]);
+
+ const handleCycleAgent = React.useCallback(() => {
+ if (primaryAgents.length <= 1) return;
+
+ const currentIndex = primaryAgents.findIndex(agent => agent.name === currentAgentName);
+ const nextIndex = (currentIndex + 1) % primaryAgents.length;
+ const nextAgent = primaryAgents[nextIndex];
+
+ setAgent(nextAgent.name);
+
+ if (currentSessionId) {
+ saveSessionAgentSelection(currentSessionId, nextAgent.name);
+ }
+ }, [primaryAgents, currentAgentName, currentSessionId, setAgent, saveSessionAgentSelection]);
+
+ const adjustTextareaHeight = React.useCallback(() => {
+ const textarea = textareaRef.current;
+ if (!textarea) {
+ return;
+ }
+
+ if (isDesktopExpanded) {
+ textarea.style.height = '100%';
+ textarea.style.maxHeight = 'none';
+ setTextareaSize(null);
+ return;
+ }
+
+ textarea.style.height = 'auto';
+
+ const view = textarea.ownerDocument?.defaultView;
+ const computedStyle = view ? view.getComputedStyle(textarea) : null;
+ const lineHeight = computedStyle ? parseFloat(computedStyle.lineHeight) : NaN;
+ const paddingTop = computedStyle ? parseFloat(computedStyle.paddingTop) : NaN;
+ const paddingBottom = computedStyle ? parseFloat(computedStyle.paddingBottom) : NaN;
+ const fallbackLineHeight = 22;
+ const fallbackPadding = 16;
+ const paddingTotal = Number.isNaN(paddingTop) || Number.isNaN(paddingBottom)
+ ? fallbackPadding
+ : paddingTop + paddingBottom;
+ const targetLineHeight = Number.isNaN(lineHeight) ? fallbackLineHeight : lineHeight;
+ const maxHeight = targetLineHeight * MAX_VISIBLE_TEXTAREA_LINES + paddingTotal;
+ const scrollHeight = textarea.scrollHeight || textarea.offsetHeight;
+ const nextHeight = Math.min(scrollHeight, maxHeight);
+
+ textarea.style.height = `${nextHeight}px`;
+ textarea.style.maxHeight = `${maxHeight}px`;
+
+ setTextareaSize((prev) => {
+ if (prev && prev.height === nextHeight && prev.maxHeight === maxHeight) {
+ return prev;
+ }
+ return { height: nextHeight, maxHeight };
+ });
+ }, [isDesktopExpanded]);
+
+ React.useLayoutEffect(() => {
+ adjustTextareaHeight();
+ }, [adjustTextareaHeight, message, isMobile]);
+
+ const updateAutocompleteState = React.useCallback((value: string, cursorPosition: number) => {
+ if (inputMode === 'shell') {
+ setShowCommandAutocomplete(false);
+ setShowFileMention(false);
+ setShowSkillAutocomplete(false);
+ return;
+ }
+
+ if (value.startsWith('/')) {
+ const firstSpace = value.indexOf(' ');
+ const firstNewline = value.indexOf('\n');
+ const commandEnd = Math.min(
+ firstSpace === -1 ? value.length : firstSpace,
+ firstNewline === -1 ? value.length : firstNewline
+ );
+
+ if (cursorPosition <= commandEnd && firstSpace === -1) {
+ const commandText = value.substring(1, commandEnd);
+ setCommandQuery(commandText);
+ setAutocompleteTab('commands');
+ setShowCommandAutocomplete(true);
+ setShowFileMention(false);
+ setShowSkillAutocomplete(false);
+ return;
+ }
+ }
+
+ setShowCommandAutocomplete(false);
+
+ const textBeforeCursor = value.substring(0, cursorPosition);
+
+ const lastSlashSymbol = textBeforeCursor.lastIndexOf('/');
+ if (lastSlashSymbol !== -1) {
+ const charBefore = lastSlashSymbol > 0 ? textBeforeCursor[lastSlashSymbol - 1] : null;
+ const textAfterSlash = textBeforeCursor.substring(lastSlashSymbol + 1);
+ const hasSeparator = textAfterSlash.includes(' ') || textAfterSlash.includes('\n');
+ const isWordBoundary = !charBefore || /\s/.test(charBefore);
+
+ if (isWordBoundary && !hasSeparator) {
+ setSkillQuery(textAfterSlash);
+ setShowSkillAutocomplete(true);
+ setShowFileMention(false);
+ return;
+ }
+ }
+
+ setShowSkillAutocomplete(false);
+ setSkillQuery('');
+
+ const lastAtSymbol = textBeforeCursor.lastIndexOf('@');
+ if (lastAtSymbol !== -1) {
+ const charBefore = lastAtSymbol > 0 ? textBeforeCursor[lastAtSymbol - 1] : null;
+ const textAfterAt = textBeforeCursor.substring(lastAtSymbol + 1);
+ const isWordBoundary = !charBefore || /\s/.test(charBefore);
+ if (isWordBoundary && !textAfterAt.includes(' ') && !textAfterAt.includes('\n')) {
+ setMentionQuery(textAfterAt);
+ setAutocompleteTab('agents');
+ setShowFileMention(true);
+ } else {
+ setShowFileMention(false);
+ }
+ } else {
+ setShowFileMention(false);
+ }
+ }, [inputMode, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete, setSkillQuery]);
+
+ const applyAutocompletePrefix = React.useCallback((prefix: '/' | '@') => {
+ const nextMessage = message.length === 0
+ ? prefix
+ : (message[0] === '/' || message[0] === '@')
+ ? `${prefix}${message.slice(1)}`
+ : `${prefix}${message}`;
+ setMessage(nextMessage);
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ const nextCursor = Math.min(nextMessage.length, textareaRef.current.value.length);
+ textareaRef.current.selectionStart = nextCursor;
+ textareaRef.current.selectionEnd = nextCursor;
+ }
+ adjustTextareaHeight();
+ updateAutocompleteState(nextMessage, nextMessage.length);
+ });
+ }, [adjustTextareaHeight, message, setMessage, updateAutocompleteState]);
+
+ const handleAutocompleteTabSelect = React.useCallback((tab: 'commands' | 'agents' | 'files') => {
+ const textarea = textareaRef.current;
+ if (isMobile && textarea) {
+ try {
+ textarea.focus({ preventScroll: true });
+ } catch {
+ textarea.focus();
+ }
+ const len = textarea.value.length;
+ try {
+ textarea.setSelectionRange(len, len);
+ } catch {
+ // ignored
+ }
+ }
+ setAutocompleteTab(tab);
+ setCommandQuery('');
+ setMentionQuery('');
+ if (tab === 'commands') {
+ applyAutocompletePrefix('/');
+ }
+ if (tab === 'agents') {
+ applyAutocompletePrefix('@');
+ }
+ if (tab === 'files') {
+ applyAutocompletePrefix('@');
+ }
+ setShowSkillAutocomplete(false);
+ setShowCommandAutocomplete(tab === 'commands');
+ setShowFileMention(tab === 'agents' || tab === 'files');
+ }, [applyAutocompletePrefix, isMobile, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]);
+
+ const handleOpenCommandMenu = React.useCallback(() => {
+ if (!isMobile) {
+ return;
+ }
+ const textarea = textareaRef.current;
+ if (textarea) {
+ try {
+ textarea.focus({ preventScroll: true });
+ } catch {
+ textarea.focus();
+ }
+ const len = textarea.value.length;
+ try {
+ textarea.setSelectionRange(len, len);
+ } catch {
+ // ignored
+ }
+ }
+ applyAutocompletePrefix('/');
+ setCommandQuery('');
+ setAutocompleteTab('commands');
+ setShowCommandAutocomplete(true);
+ setShowFileMention(false);
+ setShowSkillAutocomplete(false);
+ }, [applyAutocompletePrefix, isMobile, setAutocompleteTab, setCommandQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]);
+
+ const insertTextAtSelection = React.useCallback((text: string) => {
+ if (!text) {
+ return;
+ }
+
+ const textarea = textareaRef.current;
+ if (!textarea) {
+ const nextValue = message + text;
+ setMessage(nextValue);
+ updateAutocompleteState(nextValue, nextValue.length);
+ requestAnimationFrame(() => adjustTextareaHeight());
+ return;
+ }
+
+ const start = textarea.selectionStart ?? message.length;
+ const end = textarea.selectionEnd ?? message.length;
+ const nextValue = `${message.substring(0, start)}${text}${message.substring(end)}`;
+ setMessage(nextValue);
+ const cursorPosition = start + text.length;
+
+ requestAnimationFrame(() => {
+ const currentTextarea = textareaRef.current;
+ if (currentTextarea) {
+ currentTextarea.selectionStart = cursorPosition;
+ currentTextarea.selectionEnd = cursorPosition;
+ }
+ adjustTextareaHeight();
+ });
+
+ updateAutocompleteState(nextValue, cursorPosition);
+ }, [adjustTextareaHeight, message, updateAutocompleteState]);
+
+ const handleTextChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ const cursorPosition = e.target.selectionStart ?? value.length;
+
+ if (inputMode === 'normal' && value.startsWith('!')) {
+ const shellCommand = value.slice(1);
+ const nextCursor = Math.max(0, cursorPosition - 1);
+ setInputMode('shell');
+ setMessage(shellCommand);
+ adjustTextareaHeight();
+ setShowCommandAutocomplete(false);
+ setShowSkillAutocomplete(false);
+ setShowFileMention(false);
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = nextCursor;
+ textareaRef.current.selectionEnd = nextCursor;
+ }
+ });
+ return;
+ }
+
+ setMessage(value);
+ adjustTextareaHeight();
+ updateAutocompleteState(value, cursorPosition);
+ };
+
+ const handlePaste = React.useCallback(async (e: React.ClipboardEvent) => {
+ const fileMap = new Map();
+
+ Array.from(e.clipboardData.files || []).forEach(file => {
+ if (file.type.startsWith('image/')) {
+ fileMap.set(`${file.name}-${file.size}`, file);
+ }
+ });
+
+ Array.from(e.clipboardData.items || []).forEach(item => {
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
+ const file = item.getAsFile();
+ if (file) {
+ fileMap.set(`${file.name}-${file.size}`, file);
+ }
+ }
+ });
+
+ const imageFiles = Array.from(fileMap.values());
+ if (imageFiles.length === 0) {
+ return;
+ }
+
+ if (!currentSessionId && !newSessionDraftOpen) {
+ return;
+ }
+
+ e.preventDefault();
+
+ const pastedText = e.clipboardData.getData('text');
+ if (pastedText) {
+ insertTextAtSelection(pastedText);
+ }
+
+ let attachedCount = 0;
+
+ for (const file of imageFiles) {
+ const sizeBefore = useSessionStore.getState().attachedFiles.length;
+ try {
+ await addAttachedFile(file);
+ const sizeAfter = useSessionStore.getState().attachedFiles.length;
+ if (sizeAfter > sizeBefore) {
+ attachedCount += 1;
+ }
+ } catch (error) {
+ console.error('Clipboard image attach failed', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to attach image from clipboard');
+ }
+ }
+
+ if (attachedCount > 0) {
+ toast.success(`Attached ${attachedCount} image${attachedCount > 1 ? 's' : ''} from clipboard`);
+ }
+ }, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection]);
+
+ const handleFileSelect = (file: { name: string; path: string; relativePath?: string }) => {
+
+ const cursorPosition = textareaRef.current?.selectionStart || 0;
+ const textBeforeCursor = message.substring(0, cursorPosition);
+ const lastAtSymbol = textBeforeCursor.lastIndexOf('@');
+
+ const mentionPath = (file.relativePath && file.relativePath.trim().length > 0)
+ ? file.relativePath.trim()
+ : (toProjectRelativeMentionPath(file.path) || file.name);
+
+ if (lastAtSymbol !== -1) {
+ const newMessage =
+ message.substring(0, lastAtSymbol) +
+ `@${mentionPath} ` +
+ message.substring(cursorPosition);
+ setMessage(newMessage);
+ const nextCursor = lastAtSymbol + mentionPath.length + 2;
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = nextCursor;
+ textareaRef.current.selectionEnd = nextCursor;
+ }
+ adjustTextareaHeight();
+ updateAutocompleteState(newMessage, nextCursor);
+ });
+ } else if (textareaRef.current) {
+ const newMessage =
+ message.substring(0, cursorPosition) +
+ `@${mentionPath} ` +
+ message.substring(cursorPosition);
+ setMessage(newMessage);
+ const nextCursor = cursorPosition + mentionPath.length + 2;
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = nextCursor;
+ textareaRef.current.selectionEnd = nextCursor;
+ }
+ adjustTextareaHeight();
+ updateAutocompleteState(newMessage, nextCursor);
+ });
+ }
+
+ setShowFileMention(false);
+ setMentionQuery('');
+
+ textareaRef.current?.focus();
+ };
+
+ const handleAgentSelect = (agentName: string) => {
+ const textarea = textareaRef.current;
+ const cursorPosition = textarea?.selectionStart ?? message.length;
+ const textBeforeCursor = message.substring(0, cursorPosition);
+ const lastAtSymbol = textBeforeCursor.lastIndexOf('@');
+
+ if (lastAtSymbol !== -1) {
+ const newMessage =
+ message.substring(0, lastAtSymbol) +
+ `@${agentName} ` +
+ message.substring(cursorPosition);
+ setMessage(newMessage);
+
+ const nextCursor = lastAtSymbol + agentName.length + 2;
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = nextCursor;
+ textareaRef.current.selectionEnd = nextCursor;
+ }
+ adjustTextareaHeight();
+ updateAutocompleteState(newMessage, nextCursor);
+ });
+ } else if (textareaRef.current) {
+ const newMessage =
+ message.substring(0, cursorPosition) +
+ `@${agentName} ` +
+ message.substring(cursorPosition);
+ setMessage(newMessage);
+
+ const nextCursor = cursorPosition + agentName.length + 2;
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = nextCursor;
+ textareaRef.current.selectionEnd = nextCursor;
+ }
+ adjustTextareaHeight();
+ updateAutocompleteState(newMessage, nextCursor);
+ });
+ }
+
+ setShowFileMention(false);
+ setMentionQuery('');
+
+ textareaRef.current?.focus();
+ };
+
+ const handleSkillSelect = (skillName: string) => {
+ const textarea = textareaRef.current;
+ const cursorPosition = textarea?.selectionStart ?? message.length;
+ const textBeforeCursor = message.substring(0, cursorPosition);
+ const lastSlashSymbol = textBeforeCursor.lastIndexOf('/');
+
+ if (lastSlashSymbol !== -1) {
+ const newMessage =
+ message.substring(0, lastSlashSymbol) +
+ `/${skillName} ` +
+ message.substring(cursorPosition);
+ setMessage(newMessage);
+
+ const nextCursor = lastSlashSymbol + skillName.length + 2;
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = nextCursor;
+ textareaRef.current.selectionEnd = nextCursor;
+ }
+ adjustTextareaHeight();
+ updateAutocompleteState(newMessage, nextCursor);
+ });
+ }
+
+ setShowSkillAutocomplete(false);
+ setSkillQuery('');
+
+ textareaRef.current?.focus();
+ };
+
+ const handleCommandSelect = (command: { name: string; description?: string; agent?: string; model?: string }) => {
+
+ setMessage(`/${command.name} `);
+
+ const textareaElement = textareaRef.current as HTMLTextAreaElement & { _commandMetadata?: typeof command };
+ if (textareaElement) {
+ textareaElement._commandMetadata = command;
+ }
+
+ setShowCommandAutocomplete(false);
+ setCommandQuery('');
+
+ const refocus = () => {
+ if (textareaRef.current) {
+ try {
+ textareaRef.current.focus({ preventScroll: true });
+ } catch {
+ textareaRef.current.focus();
+ }
+ textareaRef.current.setSelectionRange(textareaRef.current.value.length, textareaRef.current.value.length);
+ }
+ };
+
+ requestAnimationFrame(() => {
+ refocus();
+ requestAnimationFrame(refocus);
+ });
+ setTimeout(refocus, 60);
+ };
+
+ React.useEffect(() => {
+
+ if (currentSessionId && textareaRef.current && !isMobile) {
+ textareaRef.current.focus();
+ }
+ }, [currentSessionId, isMobile]);
+
+ React.useEffect(() => {
+ if (!isMobile) {
+ setMobileControlsOpen(false);
+ setMobileControlsPanel(null);
+ }
+ }, [isMobile]);
+
+ React.useEffect(() => {
+ if (abortPromptSessionId && abortPromptSessionId !== currentSessionId) {
+ clearAbortPrompt();
+ }
+ }, [abortPromptSessionId, currentSessionId, clearAbortPrompt]);
+
+ React.useEffect(() => {
+ canAcceptDropRef.current = Boolean(currentSessionId || newSessionDraftOpen);
+ }, [currentSessionId, newSessionDraftOpen]);
+
+ const hasDraggedFiles = React.useCallback((dataTransfer: DataTransfer | null | undefined): boolean => {
+ if (!dataTransfer) return false;
+ if (dataTransfer.files && dataTransfer.files.length > 0) return true;
+ if (dataTransfer.types) {
+ const types = Array.from(dataTransfer.types);
+ if (types.includes('Files')) return true;
+ if (types.includes('text/uri-list')) return true;
+ }
+
+ const uriList = dataTransfer.getData('text/uri-list') || dataTransfer.getData('text/plain');
+ return typeof uriList === 'string' && uriList.toLowerCase().includes('file://');
+ }, []);
+
+ const collectDroppedFiles = React.useCallback((dataTransfer: DataTransfer | null | undefined): File[] => {
+ if (!dataTransfer) return [];
+
+ const directFiles = Array.from(dataTransfer.files || []);
+ if (directFiles.length > 0) {
+ return directFiles;
+ }
+
+ const fromItems = Array.from(dataTransfer.items || [])
+ .filter((item) => item.kind === 'file')
+ .map((item) => item.getAsFile())
+ .filter((file): file is File => Boolean(file));
+
+ return fromItems;
+ }, []);
+
+ const collectDroppedFileUris = React.useCallback((dataTransfer: DataTransfer | null | undefined): string[] => {
+ if (!dataTransfer || typeof dataTransfer.getData !== 'function') return [];
+
+ const rawUriList = dataTransfer.getData('text/uri-list') || dataTransfer.getData('text/plain');
+ if (!rawUriList) return [];
+
+ const candidates = rawUriList
+ .split(/\r?\n/)
+ .map((value) => value.trim())
+ .filter((value) => value.length > 0 && !value.startsWith('#'))
+ .filter((value) => value.toLowerCase().startsWith('file://'));
+
+ return Array.from(new Set(candidates));
+ }, []);
+
+ const attachVSCodeDroppedUris = React.useCallback(async (uris: string[]) => {
+ if (uris.length === 0) return;
+
+ try {
+ const response = await fetch('/api/vscode/drop-files', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ uris }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to attach dropped files (${response.status})`);
+ }
+
+ const data = await response.json();
+ const picked = Array.isArray(data?.files) ? data.files : [];
+ const skipped = Array.isArray(data?.skipped) ? data.skipped : [];
+
+ if (skipped.length > 0) {
+ const summary = skipped
+ .map((entry: { name?: string; reason?: string }) => `${entry?.name || 'file'}: ${entry?.reason || 'skipped'}`)
+ .join('\n');
+ toast.error(`Some dropped files were skipped:\n${summary}`);
+ }
+
+ let attachedCount = 0;
+ for (const file of picked as Array<{ name: string; mimeType?: string; dataUrl?: string }>) {
+ if (!file?.dataUrl) continue;
+
+ const sizeBefore = useSessionStore.getState().attachedFiles.length;
+ try {
+ const [meta, base64] = file.dataUrl.split(',');
+ const mime = file.mimeType || (meta?.match(/data:(.*);base64/)?.[1] || 'application/octet-stream');
+ if (!base64) continue;
+
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+
+ const blob = new Blob([bytes], { type: mime });
+ const localFile = new File([blob], file.name || 'file', { type: mime });
+ await addAttachedFile(localFile);
+
+ const sizeAfter = useSessionStore.getState().attachedFiles.length;
+ if (sizeAfter > sizeBefore) {
+ attachedCount += 1;
+ }
+ } catch (error) {
+ console.error('Dropped file attach failed', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to attach dropped file');
+ }
+ }
+
+ if (attachedCount > 0) {
+ toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`);
+ }
+ } catch (error) {
+ console.error('VS Code dropped file attach failed', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to attach dropped files');
+ }
+ }, [addAttachedFile]);
+
+ const normalizeDroppedPath = React.useCallback((rawPath: string): string => {
+ const input = rawPath.trim();
+ if (!input.toLowerCase().startsWith('file://')) {
+ return input;
+ }
+
+ try {
+ let pathname = decodeURIComponent(new URL(input).pathname || '');
+ if (/^\/[A-Za-z]:\//.test(pathname)) {
+ pathname = pathname.slice(1);
+ }
+ return pathname || input;
+ } catch {
+ const stripped = input.replace(/^file:\/\//i, '');
+ try {
+ return decodeURIComponent(stripped);
+ } catch {
+ return stripped;
+ }
+ }
+ }, []);
+
+ const toProjectRelativeMentionPath = React.useCallback((absolutePath: string): string => {
+ const normalizedAbsolutePath = absolutePath.replace(/\\/g, '/').trim();
+ const normalizedRoot = (chatSearchDirectory || '').replace(/\\/g, '/').replace(/\/+$/, '');
+ if (!normalizedRoot) {
+ return normalizedAbsolutePath;
+ }
+ if (normalizedAbsolutePath === normalizedRoot) {
+ return normalizedAbsolutePath;
+ }
+ const rootWithSlash = `${normalizedRoot}/`;
+ if (normalizedAbsolutePath.startsWith(rootWithSlash)) {
+ return normalizedAbsolutePath.slice(rootWithSlash.length);
+ }
+ return normalizedAbsolutePath;
+ }, [chatSearchDirectory]);
+
+ const handleDragEnter = (e: React.DragEvent) => {
+ if (!hasDraggedFiles(e.dataTransfer)) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ if ((currentSessionId || newSessionDraftOpen) && !isDragging) {
+ setIsDragging(true);
+ }
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ if (!hasDraggedFiles(e.dataTransfer)) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ e.dataTransfer.dropEffect = 'copy';
+ if ((currentSessionId || newSessionDraftOpen) && !isDragging) {
+ setIsDragging(true);
+ }
+ };
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.currentTarget === e.target) {
+ setIsDragging(false);
+ }
+ };
+
+ const handleDrop = async (e: React.DragEvent) => {
+ if (!hasDraggedFiles(e.dataTransfer)) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ setIsDragging(false);
+
+ if (!currentSessionId && !newSessionDraftOpen) return;
+
+ const files = collectDroppedFiles(e.dataTransfer);
+
+ if (files.length === 0 && isVSCodeRuntime()) {
+ const droppedUris = collectDroppedFileUris(e.dataTransfer);
+ if (droppedUris.length > 0) {
+ await attachVSCodeDroppedUris(droppedUris);
+ }
+ return;
+ }
+
+ let attachedCount = 0;
+
+ if (files.length > 0) {
+ for (const file of files) {
+ const sizeBefore = useSessionStore.getState().attachedFiles.length;
+ try {
+ await addAttachedFile(file);
+ const sizeAfter = useSessionStore.getState().attachedFiles.length;
+ if (sizeAfter > sizeBefore) {
+ attachedCount += 1;
+ }
+ } catch (error) {
+ console.error('File attach failed', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to attach file');
+ }
+ }
+ }
+
+ if (attachedCount > 0) {
+ toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`);
+ }
+ };
+
+ // Tauri desktop: handle native file drops via onDragDropEvent
+ React.useEffect(() => {
+ if (!isTauriShell()) return;
+ let cancelled = false;
+ let unlisten: (() => void) | null = null;
+
+ void (async () => {
+ try {
+ const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow');
+ const webviewWindow = getCurrentWebviewWindow();
+ const removeListener = await webviewWindow.onDragDropEvent(async (event) => {
+ if (!canAcceptDropRef.current) return;
+
+ const payload = (event as { payload?: unknown }).payload;
+ if (!payload || typeof payload !== 'object') return;
+
+ const typed = payload as { type?: string; paths?: string[]; position?: { x?: number; y?: number } };
+ const type = typed.type;
+ const x = typed.position?.x;
+ const y = typed.position?.y;
+
+ // Check if drop is inside the chat input area
+ const zone = dropZoneRef.current;
+ let inZone: boolean | null = null;
+ if (zone && typeof x === 'number' && typeof y === 'number') {
+ const rect = zone.getBoundingClientRect();
+ inZone = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
+ // Handle retina displays where Tauri might report physical pixels
+ if (!inZone && window.devicePixelRatio > 1) {
+ const sx = x / window.devicePixelRatio;
+ const sy = y / window.devicePixelRatio;
+ inZone = sx >= rect.left && sx <= rect.right && sy >= rect.top && sy <= rect.bottom;
+ }
+ }
+
+ if (type === 'enter' || type === 'over') {
+ if (inZone !== null) {
+ nativeDragInsideDropZoneRef.current = inZone;
+ }
+ setIsDragging(nativeDragInsideDropZoneRef.current);
+ return;
+ }
+ if (type === 'leave') {
+ nativeDragInsideDropZoneRef.current = false;
+ setIsDragging(false);
+ return;
+ }
+ if (type === 'drop') {
+ const shouldHandleDrop = inZone ?? nativeDragInsideDropZoneRef.current;
+ nativeDragInsideDropZoneRef.current = false;
+ setIsDragging(false);
+ if (!shouldHandleDrop) return;
+
+ const paths = Array.isArray(typed.paths)
+ ? typed.paths.filter((p): p is string => typeof p === 'string')
+ : [];
+ if (paths.length === 0) return;
+
+ let attachedCount = 0;
+ for (const path of paths) {
+ const sizeBefore = useSessionStore.getState().attachedFiles.length;
+ try {
+ const normalizedPath = normalizeDroppedPath(path);
+ const fileName = normalizedPath.split(/[\\/]/).pop() || normalizedPath;
+ let file: File;
+
+ // In Tauri shell, dropped paths are local machine paths.
+ // Read bytes via native command to avoid workspace-bound /api/fs/raw restrictions.
+ if (isTauriShell()) {
+ const { invoke } = await import('@tauri-apps/api/core');
+ const result = await invoke<{ mime: string; base64: string }>('desktop_read_file', { path: normalizedPath });
+ const byteCharacters = atob(result.base64);
+ const byteNumbers = new Array(byteCharacters.length);
+ for (let i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
+ }
+ const byteArray = new Uint8Array(byteNumbers);
+ const blob = new Blob([byteArray], { type: result.mime || 'application/octet-stream' });
+ file = new File([blob], fileName, { type: result.mime || 'application/octet-stream' });
+ } else {
+ const response = await fetch(`/api/fs/raw?path=${encodeURIComponent(normalizedPath)}`);
+ if (!response.ok) {
+ throw new Error(`Failed to read dropped file (${response.status})`);
+ }
+ const blob = await response.blob();
+ file = new File([blob], fileName, { type: blob.type || 'application/octet-stream' });
+ }
+
+ await addAttachedFile(file);
+ const sizeAfter = useSessionStore.getState().attachedFiles.length;
+ if (sizeAfter > sizeBefore) attachedCount++;
+ } catch (error) {
+ console.error('Failed to attach dropped file:', path, error);
+ toast.error(`Failed to attach ${path.split(/[\\/]/).pop() || 'file'}`);
+ }
+ }
+ if (attachedCount > 0) {
+ toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`);
+ }
+ }
+ });
+
+ if (cancelled) {
+ removeListener();
+ return;
+ }
+ unlisten = removeListener;
+ } catch (error) {
+ if (!cancelled) {
+ console.warn('Failed to register Tauri drag-drop listener:', error);
+ }
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ if (unlisten) unlisten();
+ };
+ }, [addAttachedFile, normalizeDroppedPath]);
+
+ const fileInputRef = React.useRef(null);
+
+ const attachFiles = React.useCallback(async (files: FileList | File[]) => {
+ let attachedCount = 0;
+ const list = Array.isArray(files) ? files : Array.from(files);
+
+ for (const file of list) {
+ const sizeBefore = useSessionStore.getState().attachedFiles.length;
+ try {
+ await addAttachedFile(file);
+ const sizeAfter = useSessionStore.getState().attachedFiles.length;
+ if (sizeAfter > sizeBefore) {
+ attachedCount += 1;
+ }
+ } catch (error) {
+ console.error('File attach failed', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to attach file');
+ }
+ }
+
+ if (attachedCount > 0) {
+ toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`);
+ }
+ }, [addAttachedFile]);
+
+ const handleVSCodePickFiles = React.useCallback(async () => {
+ try {
+ const response = await fetch('/api/vscode/pick-files');
+ const data = await response.json();
+ const picked = Array.isArray(data?.files) ? data.files : [];
+ const skipped = Array.isArray(data?.skipped) ? data.skipped : [];
+
+ if (skipped.length > 0) {
+ const summary = skipped
+ .map((s: { name?: string; reason?: string }) => `${s?.name || 'file'}: ${s?.reason || 'skipped'}`)
+ .join('\n');
+ toast.error(`Some files were skipped:\n${summary}`);
+ }
+
+ const asFiles = picked
+ .map((file: { name: string; mimeType?: string; dataUrl?: string }) => {
+ if (!file?.dataUrl) return null;
+ try {
+ const [meta, base64] = file.dataUrl.split(',');
+ const mime = file.mimeType || (meta?.match(/data:(.*);base64/)?.[1] || 'application/octet-stream');
+ if (!base64) return null;
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ const blob = new Blob([bytes], { type: mime });
+ return new File([blob], file.name || 'file', { type: mime });
+ } catch (err) {
+ console.error('Failed to decode VS Code picked file', err);
+ return null;
+ }
+ })
+ .filter(Boolean) as File[];
+
+ if (asFiles.length > 0) {
+ await attachFiles(asFiles);
+ }
+ } catch (error) {
+ console.error('VS Code file pick failed', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to pick files in VS Code');
+ }
+ }, [attachFiles]);
+
+ const handlePickLocalFiles = React.useCallback(() => {
+ if (isVSCodeRuntime()) {
+ void handleVSCodePickFiles();
+ return;
+ }
+ fileInputRef.current?.click();
+ }, [handleVSCodePickFiles]);
+
+ const handleLocalFileSelect = React.useCallback(async (event: React.ChangeEvent) => {
+ const files = event.target.files;
+ if (!files) return;
+ await attachFiles(files);
+ event.target.value = '';
+ }, [attachFiles]);
+
+ const footerGapClass = 'gap-x-1.5 gap-y-0';
+ const isVSCode = isVSCodeRuntime();
+ const footerPaddingClass = isMobile ? 'px-1.5 py-1.5' : (isVSCode ? 'px-1.5 py-1' : 'px-2.5 py-1.5');
+ const buttonSizeClass = isMobile ? 'h-8 w-8' : (isVSCode ? 'h-5 w-5' : 'h-6 w-6');
+ const sendIconSizeClass = isMobile ? 'h-4 w-4' : (isVSCode ? 'h-3.5 w-3.5' : 'h-4 w-4');
+ const stopIconSizeClass = isMobile ? 'h-6 w-6' : (isVSCode ? 'h-4 w-4' : 'h-5 w-5');
+ const iconSizeClass = isMobile ? 'h-[18px] w-[18px]' : (isVSCode ? 'h-4 w-4' : 'h-[18px] w-[18px]');
+
+ const iconButtonBaseClass = 'flex cursor-pointer items-center justify-center text-foreground transition-none outline-none focus:outline-none flex-shrink-0 disabled:cursor-not-allowed';
+ const footerIconButtonClass = cn(iconButtonBaseClass, buttonSizeClass);
+
+ // Send button - respects queue mode setting
+ const sendButton = (
+ {
+ if (!isMobile) {
+ return;
+ }
+
+ event.preventDefault();
+ handlePrimaryAction();
+ }}
+ className={cn(
+ footerIconButtonClass,
+ canSend && (currentSessionId || newSessionDraftOpen)
+ ? 'text-primary hover:text-primary'
+ : 'opacity-30'
+ )}
+ aria-label="Send message"
+ >
+
+
+ );
+
+ // Queue button for adding message to queue while working
+ const queueButton = (
+ {
+ if (isMobile) {
+ event.preventDefault();
+ }
+ handleQueueMessage();
+ }}
+ className={cn(
+ footerIconButtonClass,
+ 'absolute z-20 bottom-full left-1/2 -translate-x-1/2 mb-1',
+ hasContent && currentSessionId
+ ? 'text-primary hover:text-primary'
+ : 'opacity-30'
+ )}
+ aria-label="Queue message"
+ >
+
+
+ );
+
+ // Stop button replaces send button when working
+ const stopButton = (
+
+
+
+ );
+
+ // Action buttons area: either send button, or stop (+ optional queue button floating above)
+ const actionButtons = canAbort ? (
+
+ {hasContent && queueButton}
+ {stopButton}
+
+ ) : (
+ sendButton
+ );
+
+ const attachmentMenu = (
+ <>
+
+
+
+ {isVSCode ? (
+ handlePickLocalFiles()}
+ title="Attach files"
+ aria-label="Attach files"
+ >
+
+
+ ) : (
+
+
+
+
+
+
+
+ {
+ requestAnimationFrame(() => handlePickLocalFiles());
+ }}
+ >
+
+ Attach files
+
+ {
+ requestAnimationFrame(() => {
+ setIssuePickerOpen(true);
+ });
+ }}
+ >
+
+ Link GitHub Issue
+
+ {
+ requestAnimationFrame(() => {
+ setPrPickerOpen(true);
+ });
+ }}
+ >
+
+ Link GitHub PR
+
+
+
+ )}
+
+ >
+ );
+
+ const settingsButton = onOpenSettings ? (
+
+
+
+ ) : null;
+
+ const attachmentsControls = (
+
+ {isMobile ? (
+ {
+ if (event.pointerType === 'touch') {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }}
+ onClick={handleOpenCommandMenu}
+ title="Commands"
+ aria-label="Commands"
+ >
+
+
+ ) : null}
+ {attachmentMenu}
+ {settingsButton}
+
+ );
+
+ const workingStatusText = working.statusText;
+
+ React.useEffect(() => {
+ const pendingAbortBanner = Boolean(working.wasAborted);
+ if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) {
+ startAbortIndicator();
+ if (currentSessionId) {
+ acknowledgeSessionAbort(currentSessionId);
+ }
+ }
+ prevWasAbortedRef.current = pendingAbortBanner;
+ }, [
+ acknowledgeSessionAbort,
+ currentSessionId,
+ showAbortStatus,
+ startAbortIndicator,
+ working.wasAborted,
+ ]);
+
+ React.useEffect(() => {
+ return () => {
+ if (abortTimeoutRef.current) {
+ clearTimeout(abortTimeoutRef.current);
+ abortTimeoutRef.current = null;
+ }
+ };
+ }, []);
+
+ return (
+ <>
+
+
+ {/* Issue Picker Dialog */}
+ {
+ setLinkedIssue(issue);
+ setLinkedPr(null);
+ }}
+ />
+ {
+ setLinkedPr(pr);
+ setLinkedIssue(null);
+ }}
+ />
+ >
+ );
+};
diff --git a/ui/src/components/chat/ChatMessage.tsx b/ui/src/components/chat/ChatMessage.tsx
new file mode 100644
index 0000000..7dfe039
--- /dev/null
+++ b/ui/src/components/chat/ChatMessage.tsx
@@ -0,0 +1,1110 @@
+import React from 'react';
+import type { Message, Part } from '@opencode-ai/sdk/v2';
+import { useShallow } from 'zustand/react/shallow';
+
+import { defaultCodeDark, defaultCodeLight } from '@/lib/codeTheme';
+import { MessageFreshnessDetector } from '@/lib/messageFreshness';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useMessageStore } from '@/stores/messageStore';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { useContextStore } from '@/stores/contextStore';
+import { useDeviceInfo } from '@/lib/device';
+import { useThemeSystem } from '@/contexts/useThemeSystem';
+import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
+import { cn } from '@/lib/utils';
+
+import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatScrollManager';
+import MessageHeader from './message/MessageHeader';
+import MessageBody from './message/MessageBody';
+import type { AgentMentionInfo } from './message/types';
+import type { StreamPhase, ToolPopupContent } from './message/types';
+import { deriveMessageRole } from './message/messageRole';
+import { filterVisibleParts } from './message/partUtils';
+import { flattenAssistantTextParts } from '@/lib/messages/messageText';
+import { isLikelyProviderAuthFailure, PROVIDER_AUTH_FAILURE_MESSAGE } from '@/lib/messages/providerAuthError';
+import { FadeInOnReveal } from './message/FadeInOnReveal';
+import type { TurnGroupingContext } from './hooks/useTurnGrouping';
+import { copyTextToClipboard } from '@/lib/clipboard';
+
+const ToolOutputDialog = React.lazy(() => import('./message/ToolOutputDialog'));
+
+const TOOL_DEFAULT_EXPANSION_BY_MODE = {
+ detailed: new Set(['task', 'edit', 'multiedit', 'write', 'apply_patch', 'bash', 'todowrite']),
+ changes: new Set(['edit', 'multiedit', 'write', 'apply_patch']),
+} as const;
+
+type DefaultExpandedToolMode = keyof typeof TOOL_DEFAULT_EXPANSION_BY_MODE;
+const EXPANDED_TOOLS_CACHE_MAX = 4000;
+const expandedToolsStateCache = new Map>();
+
+const readExpandedToolsCache = (messageId: string): Set => {
+ const cached = expandedToolsStateCache.get(messageId);
+ return cached ? new Set(cached) : new Set();
+};
+
+const writeExpandedToolsCache = (messageId: string, value: Set): void => {
+ if (expandedToolsStateCache.size >= EXPANDED_TOOLS_CACHE_MAX && !expandedToolsStateCache.has(messageId)) {
+ const oldest = expandedToolsStateCache.keys().next().value;
+ if (typeof oldest === 'string') {
+ expandedToolsStateCache.delete(oldest);
+ }
+ }
+ expandedToolsStateCache.set(messageId, new Set(value));
+};
+
+const isDefaultExpandedTool = (toolName: unknown, mode: DefaultExpandedToolMode): boolean =>
+ typeof toolName === 'string' && TOOL_DEFAULT_EXPANSION_BY_MODE[mode].has(toolName.toLowerCase());
+
+function useStickyDisplayValue(value: T | null | undefined): T | null | undefined {
+ const [stickyValue, setStickyValue] = React.useState(value);
+
+ React.useEffect(() => {
+ if (value !== undefined && value !== null) {
+ setStickyValue(value);
+ }
+ }, [value]);
+
+ return value ?? stickyValue;
+}
+
+const getMessageInfoProp = (info: unknown, key: string): unknown => {
+ if (typeof info === 'object' && info !== null) {
+ return (info as Record)[key];
+ }
+ return undefined;
+};
+
+interface ChatMessageProps {
+ message: {
+ info: Message;
+ parts: Part[];
+ };
+ previousMessage?: {
+ info: Message;
+ parts: Part[];
+ };
+ nextMessage?: {
+ info: Message;
+ parts: Part[];
+ };
+ onContentChange?: (reason?: ContentChangeReason) => void;
+ animationHandlers?: AnimationHandlers;
+ scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
+ turnGroupingContext?: TurnGroupingContext;
+}
+
+const ChatMessage: React.FC = ({
+ message,
+ previousMessage,
+ nextMessage,
+ onContentChange,
+ animationHandlers,
+ turnGroupingContext,
+}) => {
+ const { isMobile, hasTouchInput } = useDeviceInfo();
+ const { currentTheme } = useThemeSystem();
+ const messageContainerRef = React.useRef(null);
+
+ const sessionState = useSessionStore(
+ useShallow((state) => ({
+ lifecyclePhase: state.messageStreamStates.get(message.info.id)?.phase ?? null,
+ isStreamingMessage: (() => {
+ const sessionId =
+ (message.info as { sessionID?: string }).sessionID ??
+ state.currentSessionId ??
+ null;
+ if (!sessionId) return false;
+ return (state.streamingMessageIds.get(sessionId) ?? null) === message.info.id;
+ })(),
+ currentSessionId: state.currentSessionId,
+ getAgentModelForSession: state.getAgentModelForSession,
+ getSessionModelSelection: state.getSessionModelSelection,
+ revertToMessage: state.revertToMessage,
+ forkFromMessage: state.forkFromMessage,
+ }))
+ );
+
+ const {
+ lifecyclePhase,
+ isStreamingMessage,
+ currentSessionId,
+ getAgentModelForSession,
+ getSessionModelSelection,
+ revertToMessage,
+ forkFromMessage,
+ } = sessionState;
+
+ const providers = useConfigStore((state) => state.providers);
+ const { showReasoningTraces, toolCallExpansion, stickyUserHeader } = useUIStore(
+ useShallow((state) => ({
+ showReasoningTraces: state.showReasoningTraces,
+ toolCallExpansion: state.toolCallExpansion,
+ stickyUserHeader: state.stickyUserHeader,
+ }))
+ );
+
+ React.useEffect(() => {
+ if (currentSessionId) {
+ MessageFreshnessDetector.getInstance().recordSessionStart(currentSessionId);
+ }
+ }, [currentSessionId]);
+
+ const [copiedCode, setCopiedCode] = React.useState(null);
+ const [copiedMessage, setCopiedMessage] = React.useState(false);
+ const [expandedTools, setExpandedTools] = React.useState>(() => readExpandedToolsCache(message.info.id));
+ const [popupContent, setPopupContent] = React.useState({
+ open: false,
+ title: '',
+ content: '',
+ });
+
+ React.useEffect(() => {
+ setExpandedTools(readExpandedToolsCache(message.info.id));
+ }, [message.info.id]);
+
+ React.useEffect(() => {
+ expandedToolsStateCache.clear();
+ setExpandedTools(new Set());
+ }, [toolCallExpansion]);
+
+ const messageRole = React.useMemo(() => deriveMessageRole(message.info), [message.info]);
+ const isUser = messageRole.isUser;
+ const useExternalUserActionsRow = isUser && (isMobile || !stickyUserHeader);
+ const showStickyInlineHoverRow = isUser && !isMobile && stickyUserHeader && !useExternalUserActionsRow;
+
+ const sessionId = message.info.sessionID;
+
+ // Subscribe to context changes so badges update immediately on mode switches.
+ const { currentContextAgent, savedSessionAgentSelection } = useContextStore(
+ useShallow((state) => ({
+ currentContextAgent: sessionId ? state.currentAgentContext.get(sessionId) : undefined,
+ savedSessionAgentSelection: sessionId ? state.sessionAgentSelections.get(sessionId) : undefined,
+ }))
+ );
+
+ const normalizedParts = React.useMemo(() => {
+ if (!isUser) {
+ return message.parts;
+ }
+
+ const keepSyntheticUserText = (text: string): boolean => {
+ const trimmed = text.trim();
+ if (trimmed.startsWith('User has requested to enter plan mode')) return true;
+ if (trimmed.startsWith('The plan at ')) return true;
+ if (trimmed.startsWith('The following tool was executed by the user')) return true;
+ return false;
+ };
+
+ return message.parts
+ .filter((part) => {
+ const synthetic = (part as unknown as { synthetic?: boolean })?.synthetic === true;
+ if (!synthetic) return true;
+ if (part.type !== 'text') return false;
+ const text = (part as unknown as { text?: unknown })?.text;
+ return typeof text === 'string' ? keepSyntheticUserText(text) : false;
+ })
+ .map((part) => {
+ const rawPart = part as Record;
+ if (rawPart.type === 'compaction') {
+ return { type: 'text', text: '/compact' } as Part;
+ }
+ if (rawPart.type === 'text') {
+ const text = typeof rawPart.text === 'string' ? rawPart.text.trim() : '';
+ if (text.startsWith('The following tool was executed by the user')) {
+ return { type: 'text', text: '/shell' } as Part;
+ }
+ }
+ return part;
+ });
+ }, [isUser, message.parts]);
+
+ const previousUserMetadata = React.useMemo(() => {
+ if (isUser || !previousMessage) {
+ return null;
+ }
+
+ const clientRole = getMessageInfoProp(previousMessage.info, 'clientRole');
+ const role = getMessageInfoProp(previousMessage.info, 'role');
+ const previousRole = typeof clientRole === 'string' ? clientRole : (typeof role === 'string' ? role : undefined);
+ if (previousRole !== 'user') {
+ return null;
+ }
+
+ const mode = getMessageInfoProp(previousMessage.info, 'mode');
+ const agent = getMessageInfoProp(previousMessage.info, 'agent');
+ const providerID = getMessageInfoProp(previousMessage.info, 'providerID');
+ const modelID = getMessageInfoProp(previousMessage.info, 'modelID');
+ const variant = getMessageInfoProp(previousMessage.info, 'variant');
+ const resolvedAgent =
+ typeof mode === 'string' && mode.trim().length > 0
+ ? mode
+ : (typeof agent === 'string' && agent.trim().length > 0 ? agent : undefined);
+ const resolvedProvider = typeof providerID === 'string' && providerID.trim().length > 0 ? providerID : undefined;
+ const resolvedModel = typeof modelID === 'string' && modelID.trim().length > 0 ? modelID : undefined;
+ const resolvedVariant = typeof variant === 'string' && variant.trim().length > 0 ? variant : undefined;
+
+ if (!resolvedAgent && !resolvedProvider && !resolvedModel && !resolvedVariant) {
+ return null;
+ }
+
+ return {
+ agentName: resolvedAgent,
+ providerId: resolvedProvider,
+ modelId: resolvedModel,
+ variant: resolvedVariant,
+ };
+ }, [isUser, previousMessage]);
+
+ const previousIsModeSwitchMessage = React.useMemo(() => {
+ if (isUser || !previousMessage) return false;
+ const parts = Array.isArray(previousMessage.parts) ? previousMessage.parts : [];
+ for (let i = 0; i < parts.length; i++) {
+ const part = parts[i] as unknown as { type?: string; text?: string; synthetic?: boolean };
+ if (part?.type !== 'text') continue;
+ if (part?.synthetic !== true) continue;
+ const text = typeof part.text === 'string' ? part.text.trim() : '';
+ if (text.startsWith('User has requested to enter plan mode') || text.startsWith('The plan at ')) {
+ return true;
+ }
+ }
+ return false;
+ }, [isUser, previousMessage]);
+
+ const agentName = React.useMemo(() => {
+ if (isUser) return undefined;
+
+ // While the assistant message is streaming, if the immediately previous user message is a
+ // synthetic mode switch, trust that mode for the badge.
+ const timeInfo = message.info.time as { completed?: number } | undefined;
+ const isCompleted = typeof timeInfo?.completed === 'number' && timeInfo.completed > 0;
+ if (!isCompleted && previousIsModeSwitchMessage && previousUserMetadata?.agentName) {
+ return previousUserMetadata.agentName;
+ }
+
+ const messageMode = getMessageInfoProp(message.info, 'mode');
+ if (typeof messageMode === 'string' && messageMode.trim().length > 0) {
+ return messageMode;
+ }
+
+ const messageAgent = getMessageInfoProp(message.info, 'agent');
+ if (typeof messageAgent === 'string' && messageAgent.trim().length > 0) {
+ return messageAgent;
+ }
+
+ if (previousUserMetadata?.agentName) {
+ return previousUserMetadata.agentName;
+ }
+
+ if (!sessionId) {
+ return undefined;
+ }
+
+ if (currentContextAgent) {
+ return currentContextAgent;
+ }
+
+ return savedSessionAgentSelection ?? undefined;
+ }, [isUser, message.info, previousIsModeSwitchMessage, previousUserMetadata, sessionId, currentContextAgent, savedSessionAgentSelection]);
+
+ const messageProviderID = !isUser ? getMessageInfoProp(message.info, 'providerID') : null;
+ const messageModelID = !isUser ? getMessageInfoProp(message.info, 'modelID') : null;
+
+ const contextModelSelection = React.useMemo(() => {
+ if (isUser || !sessionId) return null;
+
+ if (previousUserMetadata?.providerId && previousUserMetadata?.modelId) {
+ return {
+ providerId: previousUserMetadata.providerId,
+ modelId: previousUserMetadata.modelId,
+ };
+ }
+
+ if (agentName) {
+ const agentSelection = getAgentModelForSession(sessionId, agentName);
+ if (agentSelection?.providerId && agentSelection?.modelId) {
+ return agentSelection;
+ }
+ }
+
+ const sessionSelection = getSessionModelSelection(sessionId);
+ if (sessionSelection?.providerId && sessionSelection?.modelId) {
+ return sessionSelection;
+ }
+
+ return null;
+ }, [isUser, sessionId, agentName, previousUserMetadata, getAgentModelForSession, getSessionModelSelection]);
+
+ const providerID = React.useMemo(() => {
+ if (isUser) return null;
+ if (typeof messageProviderID === 'string' && messageProviderID.trim().length > 0) {
+ return messageProviderID;
+ }
+ return contextModelSelection?.providerId ?? null;
+ }, [isUser, messageProviderID, contextModelSelection]);
+
+ const modelID = React.useMemo(() => {
+ if (isUser) return null;
+ if (typeof messageModelID === 'string' && messageModelID.trim().length > 0) {
+ return messageModelID;
+ }
+ return contextModelSelection?.modelId ?? null;
+ }, [isUser, messageModelID, contextModelSelection]);
+
+ const modelName = React.useMemo(() => {
+ if (isUser) return undefined;
+
+ if (providerID && modelID && providers.length > 0) {
+ const provider = providers.find((p) => p.id === providerID);
+ if (provider?.models && Array.isArray(provider.models)) {
+ const model = provider.models.find((m: Record) => (m as Record).id === modelID);
+ const modelObj = model as Record | undefined;
+ const name = modelObj?.name;
+ return typeof name === 'string' ? name : undefined;
+ }
+ }
+
+ return undefined;
+ }, [isUser, providerID, modelID, providers]);
+
+ const modelHasVariants = React.useMemo(() => {
+ if (isUser) return false;
+ if (!providerID || !modelID) return false;
+
+ const provider = providers.find((p) => p.id === providerID);
+ if (!provider?.models || !Array.isArray(provider.models)) {
+ return false;
+ }
+
+ const model = provider.models.find((m: Record) => (m as Record).id === modelID) as
+ | { variants?: Record }
+ | undefined;
+
+ const variants = model?.variants;
+ return Boolean(variants && Object.keys(variants).length > 0);
+ }, [isUser, modelID, providerID, providers]);
+
+ const displayAgentName = useStickyDisplayValue(agentName);
+ const displayProviderIDValue = useStickyDisplayValue(providerID ?? undefined);
+ const displayModelName = useStickyDisplayValue(modelName);
+
+ const headerAgentName = displayAgentName ?? undefined;
+ const headerProviderID = displayProviderIDValue ?? null;
+ const headerModelName = displayModelName ?? undefined;
+
+ const messageCompletedAt = React.useMemo(() => {
+ const timeInfo = message.info.time as { completed?: number } | undefined;
+ return typeof timeInfo?.completed === 'number' ? timeInfo.completed : null;
+ }, [message.info.time]);
+
+ const messageCreatedAt = React.useMemo(() => {
+ const timeInfo = message.info.time as { created?: number } | undefined;
+ return typeof timeInfo?.created === 'number' ? timeInfo.created : null;
+ }, [message.info.time]);
+
+ const isMessageCompleted = React.useMemo(() => {
+ if (isUser) return true;
+ return Boolean(messageCompletedAt && messageCompletedAt > 0);
+ }, [isUser, messageCompletedAt]);
+
+ const messageFinish = React.useMemo(() => {
+ const finish = (message.info as { finish?: string }).finish;
+ return typeof finish === 'string' ? finish : undefined;
+ }, [message.info]);
+
+ const visibleParts = React.useMemo(
+ () =>
+ filterVisibleParts(normalizedParts, {
+ includeReasoning: showReasoningTraces,
+ }),
+ [normalizedParts, showReasoningTraces]
+ );
+
+ const displayParts = React.useMemo(() => {
+ if (isUser) {
+ return visibleParts;
+ }
+
+ return isMessageCompleted ? visibleParts : [];
+ }, [isUser, isMessageCompleted, visibleParts]);
+
+
+ const assistantTextParts = React.useMemo(() => {
+ if (isUser) {
+ return [];
+ }
+ return visibleParts.filter((part) => part.type === 'text');
+ }, [isUser, visibleParts]);
+
+ const toolParts = React.useMemo(() => {
+ if (isUser) {
+ return [];
+ }
+ const filtered = visibleParts.filter((part) => part.type === 'tool');
+ return filtered;
+ }, [isUser, visibleParts]);
+
+ const effectiveExpandedTools = React.useMemo(() => {
+ // 'collapsed': Activity and tools start collapsed
+ // 'activity': Activity expanded, tools collapsed
+ // 'detailed': Activity expanded, only key tools expanded
+ // 'changes': Activity expanded, only edit/diff tools expanded
+
+ if (toolCallExpansion === 'collapsed' || toolCallExpansion === 'activity') {
+ // Tools default collapsed: expandedTools contains IDs of tools that ARE expanded
+ return expandedTools;
+ }
+
+ const defaultExpansionMode =
+ toolCallExpansion === 'detailed' || toolCallExpansion === 'changes'
+ ? toolCallExpansion
+ : null;
+
+ if (!defaultExpansionMode) {
+ return expandedTools;
+ }
+
+ // 'detailed'/'changes': expand only allowlisted tools by default.
+ // expandedTools acts as a "toggled" set (XOR with defaults).
+ const defaultExpandedToolIds = new Set();
+
+ for (const part of toolParts) {
+ const toolName = (part as { tool?: unknown }).tool;
+ if (part.id && isDefaultExpandedTool(toolName, defaultExpansionMode)) {
+ defaultExpandedToolIds.add(part.id);
+ }
+ }
+
+ if (turnGroupingContext?.isFirstAssistantInTurn) {
+ for (const activity of turnGroupingContext.activityParts) {
+ if (activity.kind !== 'tool') {
+ continue;
+ }
+
+ const toolPart = activity.part as unknown as { id?: string; tool?: unknown };
+ if (isDefaultExpandedTool(toolPart.tool, defaultExpansionMode)) {
+ if (toolPart.id) {
+ defaultExpandedToolIds.add(toolPart.id);
+ }
+ if (activity.id) {
+ defaultExpandedToolIds.add(activity.id);
+ }
+ }
+ }
+ }
+
+ const effective = new Set(defaultExpandedToolIds);
+ for (const id of expandedTools) {
+ if (effective.has(id)) {
+ effective.delete(id);
+ } else {
+ effective.add(id);
+ }
+ }
+ return effective;
+ }, [expandedTools, toolCallExpansion, toolParts, turnGroupingContext]);
+
+ const agentMention = React.useMemo(() => {
+ if (!isUser) {
+ return undefined;
+ }
+ const mentionPart = message.parts.find((part) => part.type === 'agent');
+ if (!mentionPart) {
+ return undefined;
+ }
+ const partWithName = mentionPart as { name?: string; source?: { value?: string } };
+ const name = typeof partWithName.name === 'string' ? partWithName.name : undefined;
+ if (!name) {
+ return undefined;
+ }
+ const rawValue = partWithName.source && typeof partWithName.source.value === 'string' && partWithName.source.value.trim().length > 0
+ ? partWithName.source.value
+ : `@${name}`;
+ return { name, token: rawValue } satisfies AgentMentionInfo;
+ }, [isUser, message.parts]);
+
+ const shouldHideUserMessage = isUser && displayParts.length === 0;
+
+ // Message is considered to have an "open step" if info.finish is not yet present
+ const hasOpenStep = typeof messageFinish !== 'string';
+
+ const shouldCoordinateRendering = React.useMemo(() => {
+ if (isUser) {
+ return false;
+ }
+ if (assistantTextParts.length === 0 || toolParts.length === 0) {
+ return hasOpenStep;
+ }
+ return true;
+ }, [assistantTextParts.length, toolParts.length, hasOpenStep, isUser]);
+
+ const themeVariant = currentTheme?.metadata.variant;
+ const isDarkTheme = React.useMemo(() => {
+ if (themeVariant) {
+ return themeVariant === 'dark';
+ }
+ if (typeof document !== 'undefined') {
+ return document.documentElement.classList.contains('dark');
+ }
+ return false;
+ }, [themeVariant]);
+
+ const syntaxTheme = React.useMemo(() => {
+ if (currentTheme) {
+ return generateSyntaxTheme(currentTheme);
+ }
+ return isDarkTheme ? defaultCodeDark : defaultCodeLight;
+ }, [currentTheme, isDarkTheme]);
+
+ const shouldAnimateMessage = React.useMemo(() => {
+ if (isUser) return false;
+ const freshnessDetector = MessageFreshnessDetector.getInstance();
+ return freshnessDetector.shouldAnimateMessage(message.info, currentSessionId || message.info.sessionID);
+ }, [message.info, currentSessionId, isUser]);
+
+ const [hasStartedStreamingHeader, setHasStartedStreamingHeader] = React.useState(false);
+
+ const previousRole = React.useMemo(() => {
+ if (!previousMessage) return null;
+ return deriveMessageRole(previousMessage.info);
+ }, [previousMessage]);
+
+ const nextRole = React.useMemo(() => {
+ if (!nextMessage) return null;
+ return deriveMessageRole(nextMessage.info);
+ }, [nextMessage]);
+
+ const isFollowedByAssistant = React.useMemo(() => {
+ if (isUser) return false;
+ if (!nextRole) return false;
+ return !nextRole.isUser && nextRole.role === 'assistant';
+ }, [isUser, nextRole]);
+
+ const streamPhase: StreamPhase = React.useMemo(() => {
+ if (isMessageCompleted) {
+ return 'completed';
+ }
+ if (lifecyclePhase) {
+ return lifecyclePhase;
+ }
+ return isStreamingMessage ? 'streaming' : 'completed';
+ }, [isMessageCompleted, lifecyclePhase, isStreamingMessage]);
+
+ React.useEffect(() => {
+ setHasStartedStreamingHeader(false);
+ }, [message.info.id]);
+
+ React.useEffect(() => {
+ const headerMessageId = turnGroupingContext?.headerMessageId;
+ if (isUser || !headerMessageId || headerMessageId !== message.info.id) {
+ return;
+ }
+
+ const isCurrentlyStreaming = streamPhase === 'streaming' || streamPhase === 'cooldown';
+ if (isCurrentlyStreaming) {
+ setHasStartedStreamingHeader(true);
+ }
+ }, [isUser, message.info.id, streamPhase, turnGroupingContext?.headerMessageId]);
+
+ const shouldShowHeader = React.useMemo(() => {
+ if (isUser) return true;
+
+ // Use turn grouping context if available for more precise control
+ const headerMessageId = turnGroupingContext?.headerMessageId;
+ if (headerMessageId) {
+ // For turn grouping: only show header for the first assistant message in the turn
+ const isFirstAssistantInTurn = message.info.id === headerMessageId;
+
+ if (isFirstAssistantInTurn) {
+ // For completed messages, always show header (historical messages)
+ if (streamPhase === 'completed') {
+ return true;
+ }
+
+ // For streaming messages: show header when streaming starts and keep it visible
+ const isCurrentlyStreaming = streamPhase === 'streaming' || streamPhase === 'cooldown';
+ return hasStartedStreamingHeader || isCurrentlyStreaming;
+ }
+
+ // For non-first assistant messages, don't show header
+ return false;
+ }
+
+ // Fallback to original logic when turn grouping is not available
+ if (!previousRole) return true;
+ return previousRole.isUser;
+ }, [hasStartedStreamingHeader, isUser, previousRole, turnGroupingContext, streamPhase, message.info]);
+
+ const handleCopyCode = React.useCallback((code: string) => {
+ void copyTextToClipboard(code).then((result) => {
+ if (!result.ok) {
+ return;
+ }
+ setCopiedCode(code);
+ setTimeout(() => setCopiedCode(null), 2000);
+ });
+ }, []);
+
+ const userMessageIdForTurn = turnGroupingContext?.turnId;
+ const { assistantSummaryFromStore, variantFromTurnStore } = useMessageStore(
+ useShallow((state) => {
+ if (!userMessageIdForTurn || !message.info.sessionID) {
+ return { assistantSummaryFromStore: undefined, variantFromTurnStore: undefined };
+ }
+ const sessionMessages = state.messages.get(message.info.sessionID);
+ if (!sessionMessages) {
+ return { assistantSummaryFromStore: undefined, variantFromTurnStore: undefined };
+ }
+ const userMsg = sessionMessages.find((entry) => entry.info?.id === userMessageIdForTurn);
+ if (!userMsg) {
+ return { assistantSummaryFromStore: undefined, variantFromTurnStore: undefined };
+ }
+ const summary = (userMsg.info as { summary?: { body?: string | null | undefined } | null | undefined }).summary;
+ const body = summary?.body;
+ const variant = (userMsg.info as { variant?: unknown }).variant;
+ return {
+ assistantSummaryFromStore: typeof body === 'string' && body.trim().length > 0 ? body : undefined,
+ variantFromTurnStore: typeof variant === 'string' && variant.trim().length > 0 ? variant : undefined,
+ };
+ })
+ );
+
+ const headerVariantRaw = !isUser ? (variantFromTurnStore ?? previousUserMetadata?.variant) : undefined;
+
+ const headerVariant = !isUser && modelHasVariants ? (headerVariantRaw ?? 'Default') : undefined;
+
+ const assistantSummaryCandidate =
+ typeof turnGroupingContext?.summaryBody === 'string' && turnGroupingContext.summaryBody.trim().length > 0
+ ? turnGroupingContext.summaryBody
+ : assistantSummaryFromStore;
+
+ const [assistantSummaryForCopy, setAssistantSummaryForCopy] = React.useState(undefined);
+
+ React.useEffect(() => {
+ setAssistantSummaryForCopy(undefined);
+ }, [userMessageIdForTurn]);
+
+ React.useEffect(() => {
+ if (assistantSummaryCandidate && assistantSummaryCandidate.trim().length > 0) {
+ setAssistantSummaryForCopy(assistantSummaryCandidate);
+ }
+ }, [assistantSummaryCandidate]);
+
+ const assistantErrorText = React.useMemo(() => {
+ if (isUser) {
+ return undefined;
+ }
+ const errorInfo = (message.info as { error?: unknown } | undefined)?.error as
+ | { data?: { message?: unknown }; message?: unknown; name?: unknown }
+ | undefined;
+ if (!errorInfo) {
+ return undefined;
+ }
+ const dataMessage = typeof errorInfo.data?.message === 'string' ? errorInfo.data.message : undefined;
+ const errorMessage = typeof errorInfo.message === 'string' ? errorInfo.message : undefined;
+ const errorName = typeof errorInfo.name === 'string' ? errorInfo.name : undefined;
+ const detail = dataMessage || errorMessage || errorName;
+ if (!detail) {
+ return undefined;
+ }
+ if (errorName === 'SessionRetry') {
+ return `Opencode failed to send a message. Retry attempt info: \n\`${detail}\``;
+ }
+ if (isLikelyProviderAuthFailure(detail)) {
+ return PROVIDER_AUTH_FAILURE_MESSAGE;
+ }
+ return `Opencode failed to send message with error:\n\`${detail}\``;
+ }, [isUser, message.info]);
+
+ const messageTextContent = React.useMemo(() => {
+ if (isUser) {
+ const shellOutputs = displayParts
+ .filter((part): part is Part & { type: 'text'; shellAction?: { output?: unknown } } => part.type === 'text')
+ .map((part) => {
+ const output = part.shellAction?.output;
+ return typeof output === 'string' ? output.trim() : '';
+ })
+ .filter((output) => output.length > 0);
+
+ if (shellOutputs.length > 0) {
+ return shellOutputs.join('\n\n');
+ }
+
+ const shellCommands = displayParts
+ .filter((part): part is Part & { type: 'text'; shellAction?: { command?: unknown } } => part.type === 'text')
+ .map((part) => {
+ const command = part.shellAction?.command;
+ return typeof command === 'string' ? command.trim() : '';
+ })
+ .filter((command) => command.length > 0);
+
+ if (shellCommands.length > 0) {
+ return shellCommands.join('\n');
+ }
+
+ const textParts = displayParts
+ .filter((part): part is Part & { type: 'text'; text?: string; content?: string } => part.type === 'text')
+ .map((part) => {
+ const text = part.text || part.content || '';
+ return text.trim();
+ })
+ .filter((text) => text.length > 0);
+
+ const combined = textParts.join('\n');
+ return combined.replace(/\n\s*\n+/g, '\n');
+ }
+
+ if (assistantErrorText && assistantErrorText.trim().length > 0) {
+ return assistantErrorText;
+ }
+
+ if (assistantSummaryForCopy && assistantSummaryForCopy.trim().length > 0) {
+ return assistantSummaryForCopy;
+ }
+
+ return flattenAssistantTextParts(displayParts);
+ }, [assistantErrorText, assistantSummaryForCopy, displayParts, isUser]);
+
+ const hasTextContent = messageTextContent.length > 0;
+
+ const handleCopyMessage = React.useCallback(async () => {
+ const result = await copyTextToClipboard(messageTextContent);
+ if (!result.ok) {
+ return;
+ }
+ setCopiedMessage(true);
+ setTimeout(() => setCopiedMessage(false), 2000);
+ }, [messageTextContent]);
+
+ const handleRevert = React.useCallback(() => {
+ if (!sessionId || !message.info.id) return;
+ revertToMessage(sessionId, message.info.id);
+ }, [sessionId, message.info.id, revertToMessage]);
+
+ // NEW: Fork handler
+ const handleFork = React.useCallback(() => {
+ if (!sessionId || !message.info.id) return;
+ forkFromMessage(sessionId, message.info.id);
+ }, [sessionId, message.info.id, forkFromMessage]);
+
+ const handleToggleTool = React.useCallback((toolId: string) => {
+ setExpandedTools((prev) => {
+ const next = new Set(prev);
+ if (next.has(toolId)) {
+ next.delete(toolId);
+ } else {
+ next.add(toolId);
+ }
+ writeExpandedToolsCache(message.info.id, next);
+ return next;
+ });
+ }, [message.info.id]);
+
+ const resolvedAnimationHandlers = animationHandlers ?? null;
+ const hasAnnouncedAuxiliaryScrollRef = React.useRef(false);
+
+ const animationCompletedRef = React.useRef(false);
+ const hasRequestedReservationRef = React.useRef(false);
+ const animationStartNotifiedRef = React.useRef(false);
+ const hasTriggeredReservationOnceRef = React.useRef(false);
+
+ React.useEffect(() => {
+ animationCompletedRef.current = false;
+ hasRequestedReservationRef.current = false;
+ animationStartNotifiedRef.current = false;
+ hasTriggeredReservationOnceRef.current = false;
+ hasAnnouncedAuxiliaryScrollRef.current = false;
+ }, [message.info.id]);
+
+ const handleAuxiliaryContentComplete = React.useCallback(() => {
+ if (isUser) {
+ return;
+ }
+ if (hasAnnouncedAuxiliaryScrollRef.current) {
+ return;
+ }
+ hasAnnouncedAuxiliaryScrollRef.current = true;
+ onContentChange?.('structural');
+ }, [isUser, onContentChange]);
+
+ const setImagePreviewOpen = useUIStore((state) => state.setImagePreviewOpen);
+
+ const handleShowPopup = React.useCallback((content: ToolPopupContent) => {
+
+ if (content.image || content.mermaid) {
+ setPopupContent(content);
+ setImagePreviewOpen(true);
+ }
+ }, [setImagePreviewOpen]);
+
+ const handlePopupChange = React.useCallback((open: boolean) => {
+ setPopupContent((prev) => ({ ...prev, open }));
+ setImagePreviewOpen(open);
+ }, [setImagePreviewOpen]);
+
+ const isAnimationSettled = Boolean(getMessageInfoProp(message.info, 'animationSettled'));
+ const isStreamingPhase = streamPhase === 'streaming';
+
+ const hasReasoningParts = React.useMemo(() => {
+ if (isUser) {
+ return false;
+ }
+ return visibleParts.some((part) => part.type === 'reasoning');
+ }, [isUser, visibleParts]);
+
+ const allowAnimation = shouldAnimateMessage && !isAnimationSettled && !isStreamingPhase;
+ const shouldReserveAnimationSpace = !isUser && shouldAnimateMessage && assistantTextParts.length > 0 && !shouldCoordinateRendering;
+
+ React.useEffect(() => {
+ if (!resolvedAnimationHandlers?.onStreamingCandidate) {
+ return;
+ }
+
+ if (!shouldReserveAnimationSpace) {
+ if (hasRequestedReservationRef.current) {
+ if (hasReasoningParts && resolvedAnimationHandlers?.onReasoningBlock) {
+ resolvedAnimationHandlers.onReasoningBlock();
+ } else if (resolvedAnimationHandlers?.onReservationCancelled) {
+ resolvedAnimationHandlers.onReservationCancelled();
+ }
+ hasRequestedReservationRef.current = false;
+ }
+ return;
+ }
+
+ if (hasTriggeredReservationOnceRef.current) {
+ return;
+ }
+
+ hasTriggeredReservationOnceRef.current = true;
+ resolvedAnimationHandlers.onStreamingCandidate();
+ hasRequestedReservationRef.current = true;
+ }, [resolvedAnimationHandlers, shouldReserveAnimationSpace, hasReasoningParts]);
+
+ React.useEffect(() => {
+ if (!resolvedAnimationHandlers?.onAnimationStart) {
+ return;
+ }
+ if (!allowAnimation) {
+ return;
+ }
+ if (animationStartNotifiedRef.current) {
+ return;
+ }
+ resolvedAnimationHandlers.onAnimationStart();
+ animationStartNotifiedRef.current = true;
+ }, [resolvedAnimationHandlers, allowAnimation]);
+
+ React.useEffect(() => {
+ if (isUser) {
+ return;
+ }
+
+ const handler = resolvedAnimationHandlers?.onAnimatedHeightChange;
+ if (!handler) {
+ return;
+ }
+
+ const shouldTrackHeight = allowAnimation || shouldReserveAnimationSpace;
+ if (!shouldTrackHeight) {
+ return;
+ }
+
+ const element = messageContainerRef.current;
+ if (!element) {
+ return;
+ }
+
+ if (typeof window === 'undefined' || typeof ResizeObserver === 'undefined') {
+ handler(element.getBoundingClientRect().height);
+ return;
+ }
+
+ let rafId: number | null = null;
+ const notifyHeight = (height: number) => {
+ if (typeof window === 'undefined') {
+ handler(height);
+ return;
+ }
+ if (rafId !== null) {
+ window.cancelAnimationFrame(rafId);
+ }
+ rafId = window.requestAnimationFrame(() => {
+ handler(height);
+ });
+ };
+
+ const observer = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (!entry) {
+ return;
+ }
+ notifyHeight(entry.contentRect.height);
+ });
+
+ observer.observe(element);
+ notifyHeight(element.getBoundingClientRect().height);
+
+ return () => {
+ if (rafId !== null) {
+ window.cancelAnimationFrame(rafId);
+ rafId = null;
+ }
+ observer.disconnect();
+ };
+ }, [allowAnimation, isUser, resolvedAnimationHandlers, shouldReserveAnimationSpace]);
+
+ if (shouldHideUserMessage) {
+ return null;
+ }
+
+ const assistantTopPaddingClass = !isUser && shouldShowHeader
+ ? (stickyUserHeader ? (isMobile ? 'pt-4' : 'pt-6') : 'pt-0')
+ : 'pt-0';
+
+ return (
+ <>
+
+
+ {isUser ? (
+ displayParts.length === 0 ? null : (
+
+
+
+
+
+
+ {useExternalUserActionsRow ? (
+
+ ) : null}
+
+ {showStickyInlineHoverRow ?
: null}
+
+
+ )
+ ) : (
+
+ {shouldShowHeader && (
+
+ )}
+
+
+
+
+ )}
+
+
+
+
+
+ >
+ );
+};
+
+export default React.memo(ChatMessage);
diff --git a/ui/src/components/chat/CommandAutocomplete.tsx b/ui/src/components/chat/CommandAutocomplete.tsx
new file mode 100644
index 0000000..97565ec
--- /dev/null
+++ b/ui/src/components/chat/CommandAutocomplete.tsx
@@ -0,0 +1,419 @@
+import React from 'react';
+import { RiCommandLine, RiFileLine, RiFlashlightLine, RiRefreshLine, RiScissorsLine, RiTerminalBoxLine, RiArrowGoBackLine, RiArrowGoForwardLine, RiTimeLine } from '@remixicon/react';
+import { cn, fuzzyMatch } from '@/lib/utils';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useCommandsStore } from '@/stores/useCommandsStore';
+import { useSkillsStore } from '@/stores/useSkillsStore';
+import { useShallow } from 'zustand/react/shallow';
+import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
+
+interface CommandInfo {
+ name: string;
+ description?: string;
+ agent?: string;
+ model?: string;
+ isBuiltIn?: boolean;
+ isSkill?: boolean;
+ scope?: string;
+}
+
+export interface CommandAutocompleteHandle {
+ handleKeyDown: (key: string) => void;
+}
+
+type AutocompleteTab = 'commands' | 'agents' | 'files';
+
+interface CommandAutocompleteProps {
+ searchQuery: string;
+ onCommandSelect: (command: CommandInfo, options?: { dismissKeyboard?: boolean }) => void;
+ onClose: () => void;
+ showTabs?: boolean;
+ activeTab?: AutocompleteTab;
+ onTabSelect?: (tab: AutocompleteTab) => void;
+ style?: React.CSSProperties;
+}
+
+export const CommandAutocomplete = React.forwardRef(({
+ searchQuery,
+ onCommandSelect,
+ onClose,
+ showTabs,
+ activeTab = 'commands',
+ onTabSelect,
+ style,
+}, ref) => {
+ const { hasMessagesInCurrentSession, currentSessionId } = useSessionStore(
+ useShallow((state) => {
+ const sessionId = state.currentSessionId;
+ const messageCount = sessionId ? (state.messages.get(sessionId)?.length ?? 0) : 0;
+ return {
+ hasMessagesInCurrentSession: messageCount > 0,
+ currentSessionId: sessionId,
+ };
+ })
+ );
+ const hasSession = Boolean(currentSessionId);
+
+ const [commands, setCommands] = React.useState([]);
+ const [loading, setLoading] = React.useState(false);
+ const { commands: commandsWithMetadata, loadCommands: refreshCommands } = useCommandsStore();
+ const { skills, loadSkills: refreshSkills } = useSkillsStore();
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
+ const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
+ const containerRef = React.useRef(null);
+ const ignoreClickRef = React.useRef(false);
+ const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
+ const pointerMovedRef = React.useRef(false);
+ const ignoreTabClickRef = React.useRef(false);
+
+ React.useEffect(() => {
+ const handlePointerDown = (event: MouseEvent | TouchEvent) => {
+ const target = event.target as Node | null;
+ if (!target || !containerRef.current) {
+ return;
+ }
+ if (containerRef.current.contains(target)) {
+ return;
+ }
+ onClose();
+ };
+
+ document.addEventListener('pointerdown', handlePointerDown, true);
+ return () => {
+ document.removeEventListener('pointerdown', handlePointerDown, true);
+ };
+ }, [onClose]);
+
+ React.useEffect(() => {
+ // Force refresh to get latest project context when mounting
+ void refreshCommands();
+ void refreshSkills();
+ }, [refreshCommands, refreshSkills]);
+
+ React.useEffect(() => {
+ const loadCommands = async () => {
+ setLoading(true);
+ try {
+ const skillNames = new Set(skills.map((skill) => skill.name));
+ const customCommands: CommandInfo[] = commandsWithMetadata.map(cmd => ({
+ name: cmd.name,
+ description: cmd.description,
+ agent: cmd.agent ?? undefined,
+ model: cmd.model ?? undefined,
+ isBuiltIn: cmd.name === 'init' || cmd.name === 'review',
+ isSkill: skillNames.has(cmd.name),
+ scope: cmd.scope,
+ }));
+
+ const builtInCommands: CommandInfo[] = [
+ ...(hasSession && !hasMessagesInCurrentSession
+ ? [{ name: 'init', description: 'Create/update AGENTS.md file', isBuiltIn: true }]
+ : []
+ ),
+ ...(hasSession // Show when session exists, not when hasMessages
+ ? [
+ { name: 'undo', description: 'Undo the last message', isBuiltIn: true },
+ { name: 'redo', description: 'Redo previously undone messages', isBuiltIn: true },
+ { name: 'timeline', description: 'Jump to a specific message', isBuiltIn: true },
+ ]
+ : []
+ ),
+ { name: 'compact', description: 'Compress session history using AI to reduce context size', isBuiltIn: true },
+ ];
+
+ const commandMap = new Map();
+
+ builtInCommands.forEach(cmd => commandMap.set(cmd.name, cmd));
+
+ customCommands.forEach(cmd => commandMap.set(cmd.name, cmd));
+
+ const allCommands = Array.from(commandMap.values());
+
+ const allowInitCommand = !hasMessagesInCurrentSession;
+ const filtered = (searchQuery
+ ? allCommands.filter(cmd =>
+ fuzzyMatch(cmd.name, searchQuery) ||
+ (cmd.description && fuzzyMatch(cmd.description, searchQuery))
+ )
+ : allCommands).filter(cmd => allowInitCommand || cmd.name !== 'init');
+
+ filtered.sort((a, b) => {
+ const aStartsWith = a.name.toLowerCase().startsWith(searchQuery.toLowerCase());
+ const bStartsWith = b.name.toLowerCase().startsWith(searchQuery.toLowerCase());
+ if (aStartsWith && !bStartsWith) return -1;
+ if (!aStartsWith && bStartsWith) return 1;
+ return a.name.localeCompare(b.name);
+ });
+
+ setCommands(filtered);
+ } catch {
+
+ const allowInitCommand = !hasMessagesInCurrentSession;
+ const builtInCommands: CommandInfo[] = [
+ ...(hasSession && !hasMessagesInCurrentSession
+ ? [{ name: 'init', description: 'Create/update AGENTS.md file', isBuiltIn: true }]
+ : []
+ ),
+ ...(hasSession // Show when session exists, not when hasMessages
+ ? [
+ { name: 'undo', description: 'Undo the last message', isBuiltIn: true },
+ { name: 'redo', description: 'Redo previously undone messages', isBuiltIn: true },
+ { name: 'timeline', description: 'Jump to a specific message', isBuiltIn: true },
+ ]
+ : []
+ ),
+ { name: 'compact', description: 'Compress session history using AI to reduce context size', isBuiltIn: true },
+ ];
+
+ const filtered = (searchQuery
+ ? builtInCommands.filter(cmd =>
+ fuzzyMatch(cmd.name, searchQuery) ||
+ (cmd.description && fuzzyMatch(cmd.description, searchQuery))
+ )
+ : builtInCommands).filter(cmd => allowInitCommand || cmd.name !== 'init');
+
+ setCommands(filtered);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadCommands();
+ }, [searchQuery, hasMessagesInCurrentSession, hasSession, commandsWithMetadata, skills]);
+
+ React.useEffect(() => {
+ setSelectedIndex(0);
+ }, [commands]);
+
+ React.useEffect(() => {
+ itemRefs.current[selectedIndex]?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest'
+ });
+ }, [selectedIndex]);
+
+ React.useImperativeHandle(ref, () => ({
+ handleKeyDown: (key: string) => {
+ const total = commands.length;
+ if (key === 'Escape') {
+ onClose();
+ return;
+ }
+
+ if (total === 0) {
+ return;
+ }
+
+ if (key === 'ArrowDown') {
+ setSelectedIndex((prev) => (prev + 1) % total);
+ return;
+ }
+
+ if (key === 'ArrowUp') {
+ setSelectedIndex((prev) => (prev - 1 + total) % total);
+ return;
+ }
+
+ if (key === 'Enter' || key === 'Tab') {
+ const safeIndex = ((selectedIndex % total) + total) % total;
+ const command = commands[safeIndex];
+ if (command) {
+ onCommandSelect(command);
+ }
+ }
+ }
+ }), [commands, selectedIndex, onClose, onCommandSelect]);
+
+ const getCommandIcon = (command: CommandInfo) => {
+
+ switch (command.name) {
+ case 'init':
+ return ;
+ case 'undo':
+ return ;
+ case 'redo':
+ return ;
+ case 'timeline':
+ return ;
+ case 'compact':
+ return ;
+ case 'test':
+ case 'build':
+ case 'run':
+ return ;
+ default:
+ if (command.isBuiltIn) {
+ return ;
+ }
+ return ;
+ }
+ };
+
+ return (
+
+ {showTabs ? (
+
+
+ {([
+ { id: 'commands' as const, label: 'Commands' },
+ { id: 'agents' as const, label: 'Agents' },
+ { id: 'files' as const, label: 'Files' },
+ ]).map((tab) => (
+ {
+ if (event.pointerType !== 'touch') {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ ignoreTabClickRef.current = true;
+ onTabSelect?.(tab.id);
+ }}
+ onClick={() => {
+ if (ignoreTabClickRef.current) {
+ ignoreTabClickRef.current = false;
+ return;
+ }
+ onTabSelect?.(tab.id);
+ }}
+ >
+ {tab.label}
+
+ ))}
+
+
+ ) : null}
+
+ {loading ? (
+
+
+
+ ) : (
+
+ {commands.map((command, index) => {
+ const isSystem = command.isBuiltIn;
+ const isProject = command.scope === 'project';
+
+ return (
+
{ itemRefs.current[index] = el; }}
+ className={cn(
+ "flex items-start gap-2 px-3 py-2 cursor-pointer rounded-lg",
+ index === selectedIndex && "bg-interactive-selection"
+ )}
+ onPointerDown={(event) => {
+ if (event.pointerType !== 'touch') {
+ return;
+ }
+ pointerStartRef.current = { x: event.clientX, y: event.clientY };
+ pointerMovedRef.current = false;
+ }}
+ onPointerMove={(event) => {
+ if (event.pointerType !== 'touch' || !pointerStartRef.current) {
+ return;
+ }
+ const dx = event.clientX - pointerStartRef.current.x;
+ const dy = event.clientY - pointerStartRef.current.y;
+ if (Math.hypot(dx, dy) > 6) {
+ pointerMovedRef.current = true;
+ }
+ }}
+ onPointerUp={(event) => {
+ if (event.pointerType !== 'touch') {
+ return;
+ }
+ const didMove = pointerMovedRef.current;
+ pointerStartRef.current = null;
+ pointerMovedRef.current = false;
+ if (didMove) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ ignoreClickRef.current = true;
+ onCommandSelect(command, { dismissKeyboard: true });
+ }}
+ onPointerCancel={() => {
+ pointerStartRef.current = null;
+ pointerMovedRef.current = false;
+ }}
+ onClick={() => {
+ if (ignoreClickRef.current) {
+ ignoreClickRef.current = false;
+ return;
+ }
+ onCommandSelect(command);
+ }}
+ onMouseEnter={() => setSelectedIndex(index)}
+ >
+
+ {getCommandIcon(command)}
+
+
+
+ /{command.name}
+ {command.isSkill ? (
+
+ skill
+
+ ) : null}
+ {isSystem ? (
+
+ system
+
+ ) : command.scope ? (
+
+ {command.scope}
+
+ ) : null}
+ {command.agent && (
+
+ {command.agent}
+
+ )}
+
+ {command.description && (
+
+ {command.description}
+
+ )}
+
+
+ );
+ })}
+ {commands.length === 0 && (
+
+ No commands found
+
+ )}
+
+ )}
+
+
+ ↑↓ navigate • Enter select • Esc close
+
+
+ );
+});
+
+CommandAutocomplete.displayName = 'CommandAutocomplete';
+
+export type { CommandInfo };
diff --git a/ui/src/components/chat/DiffPreview.tsx b/ui/src/components/chat/DiffPreview.tsx
new file mode 100644
index 0000000..7c144f6
--- /dev/null
+++ b/ui/src/components/chat/DiffPreview.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { cn } from '@/lib/utils';
+import { getLanguageFromExtension } from '@/lib/toolHelpers';
+import { parseDiffToUnified } from './message/toolRenderers';
+
+interface DiffPreviewProps {
+ diff: string;
+ syntaxTheme: { [key: string]: React.CSSProperties };
+ filePath?: string;
+}
+
+export const DiffPreview: React.FC = ({ diff, syntaxTheme, filePath }) => (
+
+ {parseDiffToUnified(diff).map((hunk, hunkIdx) => (
+
+
+ {`${hunk.file || filePath?.split('/').pop() || 'file'} (line ${hunk.oldStart})`}
+
+
+
+ {hunk.lines.map((line, lineIdx) => (
+
+
+ {line.lineNumber || ''}
+
+
+
+ {line.content}
+
+
+
+ ))}
+
+
+ ))}
+
+);
+
+interface WritePreviewProps {
+ content: string;
+ syntaxTheme: { [key: string]: React.CSSProperties };
+ filePath?: string;
+}
+
+export const WritePreview: React.FC = ({ content, syntaxTheme, filePath }) => {
+ const lines = content.split('\n');
+ const language = getLanguageFromExtension(filePath ?? '') || 'text';
+ const displayPath = filePath?.split('/').pop() || 'New file';
+ const lineCount = Math.max(lines.length, 1);
+ const headerLineLabel = lineCount === 1 ? 'line 1' : `lines 1-${lineCount}`;
+
+ return (
+
+
+ {`${displayPath} (${headerLineLabel})`}
+
+
+ {lines.map((line, lineIdx) => (
+
+
+ {lineIdx + 1}
+
+
+
+ {line || ' '}
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/ui/src/components/chat/FileAttachment.tsx b/ui/src/components/chat/FileAttachment.tsx
new file mode 100644
index 0000000..f9b8a4e
--- /dev/null
+++ b/ui/src/components/chat/FileAttachment.tsx
@@ -0,0 +1,606 @@
+import React, { useRef, memo } from 'react';
+import { RiAttachment2, RiCloseLine, RiFileImageLine, RiFileLine, RiFilePdfLine } from '@remixicon/react';
+import { useSessionStore, type AttachedFile } from '@/stores/useSessionStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { toast } from '@/components/ui';
+import { cn } from '@/lib/utils';
+import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
+import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
+import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
+
+import type { ToolPopupContent } from './message/types';
+
+export const FileAttachmentButton = memo(() => {
+ const fileInputRef = useRef(null);
+ const { addAttachedFile } = useSessionStore();
+ const { isMobile } = useUIStore();
+ const isVSCodeRuntime = useIsVSCodeRuntime();
+ const buttonSizeClass = isMobile ? 'h-9 w-9' : 'h-7 w-7';
+ const iconSizeClass = isMobile ? 'h-5 w-5' : 'h-[18px] w-[18px]';
+
+ const attachFiles = async (files: FileList | File[]) => {
+ let attachedCount = 0;
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+ const sizeBefore = useSessionStore.getState().attachedFiles.length;
+ try {
+ await addAttachedFile(file);
+ const sizeAfter = useSessionStore.getState().attachedFiles.length;
+ if (sizeAfter > sizeBefore) {
+ attachedCount++;
+ }
+ } catch (error) {
+ console.error('File attach failed', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to attach file');
+ }
+ }
+ if (attachedCount > 0) {
+ toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`);
+ }
+ };
+
+ const handleFileSelect = async (e: React.ChangeEvent) => {
+ const files = e.target.files;
+ if (!files) return;
+ await attachFiles(files);
+
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ const handleVSCodePick = async () => {
+ try {
+ const response = await fetch('/api/vscode/pick-files');
+ const data = await response.json();
+ const picked = Array.isArray(data?.files) ? data.files : [];
+ const skipped = Array.isArray(data?.skipped) ? data.skipped : [];
+
+ if (skipped.length > 0) {
+ const summary = skipped.map((s: { name?: string; reason?: string }) => `${s?.name || 'file'}: ${s?.reason || 'skipped'}`).join('\n');
+ toast.error(`Some files were skipped:\n${summary}`);
+ }
+
+ const asFiles = picked
+ .map((file: { name: string; mimeType?: string; dataUrl?: string }) => {
+ if (!file?.dataUrl) return null;
+ try {
+ const [meta, base64] = file.dataUrl.split(',');
+ const mime = file.mimeType || (meta?.match(/data:(.*);base64/)?.[1] || 'application/octet-stream');
+ if (!base64) return null;
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ const blob = new Blob([bytes], { type: mime });
+ return new File([blob], file.name || 'file', { type: mime });
+ } catch (err) {
+ console.error('Failed to decode VS Code picked file', err);
+ return null;
+ }
+ })
+ .filter(Boolean) as File[];
+
+ if (asFiles.length > 0) {
+ await attachFiles(asFiles);
+ }
+ } catch (error) {
+ console.error('VS Code file pick failed', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to pick files in VS Code');
+ }
+ };
+
+ return (
+ <>
+
+
+
+ fileInputRef.current?.click()}
+ className={cn(
+ 'flex items-center justify-center rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
+ 'hover:bg-muted text-muted-foreground',
+ buttonSizeClass
+ )}
+ aria-label="Attach files"
+ >
+
+
+
+
+ Attach files
+
+
+ >
+ );
+});
+
+FileAttachmentButton.displayName = 'FileAttachmentButton';
+
+interface ImagePreviewProps {
+ file: AttachedFile;
+ onRemove: () => void;
+}
+
+const ImagePreview = memo(({ file, onRemove }: ImagePreviewProps) => {
+ const isLocalImagePreview =
+ file.source !== 'server' &&
+ file.mimeType.startsWith('image/') &&
+ typeof file.dataUrl === 'string' &&
+ file.dataUrl.startsWith('data:image/');
+
+ const imageUrl = isLocalImagePreview ? file.dataUrl : (file.serverPath || '');
+
+ const extractFilename = (path: string): string => {
+ const normalized = path.replace(/\\/g, '/');
+ const parts = normalized.split('/');
+ return parts[parts.length - 1] || path;
+ };
+
+ const getFileExtension = (filename: string): string => {
+ const parts = filename.split('.');
+ return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
+ };
+
+ const displayName = extractFilename(file.filename);
+ const extension = getFileExtension(file.filename);
+
+ if (!imageUrl) {
+ // Fallback to text-only for server images without preview
+ return (
+
+
+
+ {displayName}
+
+ {
+ e.stopPropagation();
+ onRemove();
+ }}
+ className="flex items-center justify-center h-5 w-5 flex-shrink-0 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer"
+ aria-label={`Remove ${displayName}`}
+ >
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ );
+});
+
+ImagePreview.displayName = 'ImagePreview';
+
+interface FileChipProps {
+ file: AttachedFile;
+ onRemove: () => void;
+}
+
+const FileChip = memo(({ file, onRemove }: FileChipProps) => {
+ const getFileExtension = (filename: string): string => {
+ const parts = filename.split('.');
+ return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
+ };
+
+ const formatFileSize = (bytes: number) => {
+ if (!Number.isFinite(bytes) || bytes <= 0) return '';
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
+ };
+
+ const extractFilename = (path: string): string => {
+ const normalized = path.replace(/\\/g, '/');
+ const parts = normalized.split('/');
+ const filename = parts[parts.length - 1];
+ return filename || path;
+ };
+
+ const displayName = extractFilename(file.filename);
+ const fileSize = formatFileSize(file.size);
+ const extension = getFileExtension(file.filename);
+
+ return (
+ {
+ // Prevent click from bubbling if clicking the remove button
+ if ((e.target as HTMLElement).closest('[data-remove-button]')) {
+ return;
+ }
+ }}
+ className="flex items-center gap-1.5 text-sm hover:opacity-80 transition-opacity text-left h-5"
+ >
+
+
+ {displayName}
+ {fileSize && ({fileSize}) }
+
+ {
+ e.stopPropagation();
+ onRemove();
+ }}
+ className="flex items-center justify-center h-5 w-5 flex-shrink-0 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer"
+ aria-label={`Remove ${displayName}`}
+ >
+
+
+
+ );
+});
+
+FileChip.displayName = 'FileChip';
+
+export const AttachedFilesList = memo(() => {
+ const { attachedFiles, removeAttachedFile } = useSessionStore();
+
+ const localFiles = attachedFiles.filter((file) => file.source !== 'server');
+
+ if (localFiles.length === 0) return null;
+
+ const images = localFiles.filter((f) => f.mimeType.startsWith('image/'));
+ const otherFiles = localFiles.filter((f) => !f.mimeType.startsWith('image/'));
+
+ return (
+
+ {/* Images row - inline with previews */}
+ {images.length > 0 && (
+
+ {images.map((file) => (
+ removeAttachedFile(file.id)}
+ />
+ ))}
+
+ )}
+
+ {/* Other files row - inline text-only */}
+ {otherFiles.length > 0 && (
+
+ {otherFiles.map((file) => (
+ removeAttachedFile(file.id)}
+ />
+ ))}
+
+ )}
+
+ );
+});
+
+AttachedFilesList.displayName = 'AttachedFilesList';
+
+interface FilePart {
+ type: string;
+ mime?: string;
+ url?: string;
+ filename?: string;
+ size?: number;
+}
+
+interface MessageFilesDisplayProps {
+ files: FilePart[];
+ onShowPopup?: (content: ToolPopupContent) => void;
+ compact?: boolean;
+}
+
+export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }: MessageFilesDisplayProps) => {
+
+ const fileItems = files.filter(f => f.type === 'file' && (f.mime || f.url));
+
+ const extractFilename = (path?: string): string => {
+ if (!path) return 'Unnamed file';
+
+ const normalized = path.replace(/\\/g, '/');
+ const parts = normalized.split('/');
+ const filename = parts[parts.length - 1];
+
+ return filename || path;
+ };
+
+ const formatFileSize = (bytes?: number) => {
+ if (!bytes || !Number.isFinite(bytes) || bytes <= 0) return '';
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ };
+
+ const imageFiles = fileItems.filter(f => f.mime?.startsWith('image/') && f.url);
+ const otherFiles = fileItems.filter(f => !f.mime?.startsWith('image/'));
+
+ const imageGallery = React.useMemo(
+ () =>
+ imageFiles.flatMap((file) => {
+ if (!file.url) return [];
+ const filename = extractFilename(file.filename) || 'Image';
+ return [{
+ url: file.url,
+ mimeType: file.mime,
+ filename,
+ size: file.size,
+ }];
+ }),
+ [imageFiles]
+ );
+
+ const handleImageClick = React.useCallback((index: number) => {
+ if (!onShowPopup) {
+ return;
+ }
+
+ const file = imageGallery[index];
+ if (!file?.url) return;
+
+ const filename = file.filename || 'Image';
+
+ onShowPopup({
+ open: true,
+ title: filename,
+ content: '',
+ metadata: {
+ tool: 'image-preview',
+ filename,
+ mime: file.mimeType,
+ size: file.size,
+ },
+ image: {
+ url: file.url,
+ mimeType: file.mimeType,
+ filename,
+ size: file.size,
+ gallery: imageGallery,
+ index,
+ },
+ });
+ }, [imageGallery, onShowPopup]);
+
+ if (fileItems.length === 0) return null;
+
+ if (compact) {
+ return (
+
+ {otherFiles.length > 0 && (
+
+ {otherFiles.map((file, index) => {
+ const fileName = extractFilename(file.filename || file.url);
+ const sizeText = formatFileSize(file.size);
+ return (
+
+
+
+ {file.mime?.includes('pdf') ? (
+
+ ) : (
+
+ )}
+
+ {fileName}
+
+
+
+
+ {fileName}{sizeText ? ` (${sizeText})` : ''}
+
+
+ );
+ })}
+
+ )}
+
+ {imageFiles.length > 0 && (
+
+
+ {imageFiles.map((file, index) => {
+ const filename = extractFilename(file.filename) || 'Image';
+
+ return (
+
+
+ handleImageClick(index)}
+ className="relative flex-none border border-border/40 bg-muted/10 overflow-hidden snap-start h-12 w-12 sm:h-14 sm:w-14 md:h-16 md:w-16 rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary"
+ aria-label={filename}
+ >
+ {file.url ? (
+ {
+ const target = e.target as HTMLImageElement;
+ target.style.visibility = 'hidden';
+ }}
+ />
+ ) : (
+
+
+
+ )}
+ {filename}
+
+
+
+ {filename}
+
+
+ );
+ })}
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+ {fileItems.map((file, index) => {
+ const fileName = extractFilename(file.filename || file.url);
+ const isImage = file.mime?.startsWith('image/');
+ const sizeText = formatFileSize(file.size);
+
+ if (isImage && file.url) {
+ return (
+
+
+
+
+
{fileName}
+ {sizeText &&
{sizeText}
}
+
+
+ );
+ }
+
+ return (
+
+
+ {
+ if (onShowPopup && file.url) {
+ onShowPopup({
+ open: true,
+ title: fileName,
+ content: '',
+ image: {
+ url: file.url,
+ mimeType: file.mime,
+ filename: fileName,
+ },
+ });
+ }
+ }}
+ className={cn(
+ "flex items-center gap-2 p-2 rounded-lg border border-border/40 bg-muted/10 hover:bg-muted/20 transition-colors text-left",
+ compact ? "text-xs" : "text-sm"
+ )}
+ >
+
+ {file.mime?.startsWith('image/') ? (
+
+ ) : file.mime?.includes('pdf') ? (
+
+ ) : (
+
+ )}
+
+
+
{fileName}
+ {sizeText &&
{sizeText}
}
+
+
+
+
+ {fileName}{sizeText ? ` (${sizeText})` : ''}
+
+
+ );
+ })}
+
+ );
+});
+
+MessageFilesDisplay.displayName = 'MessageFilesDisplay';
+
+interface ImageGalleryProps {
+ urls: string[];
+ caption?: string;
+ onShowPopup?: (content: ToolPopupContent) => void;
+}
+
+export const ImageGallery = memo(({ urls, caption, onShowPopup }: ImageGalleryProps) => {
+ if (urls.length === 0) return null;
+
+ const getGridCols = () => {
+ if (urls.length === 1) return 'grid-cols-1';
+ if (urls.length === 2) return 'grid-cols-2';
+ if (urls.length <= 4) return 'grid-cols-2';
+ return 'grid-cols-3';
+ };
+
+ return (
+
+
+ {urls.map((url, index) => (
+
onShowPopup?.({
+ open: true,
+ title: caption || `Image ${index + 1} of ${urls.length}`,
+ content: '',
+ image: {
+ url,
+ gallery: urls.map(u => ({ url: u })),
+ index,
+ },
+ })}
+ className="relative aspect-square rounded-lg border border-border/40 bg-muted/10 overflow-hidden group"
+ >
+
+
+
+ ))}
+
+ {caption && (
+
{caption}
+ )}
+
+ );
+});
+
+ImageGallery.displayName = 'ImageGallery';
diff --git a/ui/src/components/chat/FileMentionAutocomplete.tsx b/ui/src/components/chat/FileMentionAutocomplete.tsx
new file mode 100644
index 0000000..051d78c
--- /dev/null
+++ b/ui/src/components/chat/FileMentionAutocomplete.tsx
@@ -0,0 +1,569 @@
+import React from 'react';
+import { RiCodeLine, RiFileImageLine, RiFileLine, RiFilePdfLine, RiRefreshLine } from '@remixicon/react';
+import { cn, truncatePathMiddle } from '@/lib/utils';
+import { useFileSearchStore } from '@/stores/useFileSearchStore';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useProjectsStore } from '@/stores/useProjectsStore';
+import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
+import { useDebouncedValue } from '@/hooks/useDebouncedValue';
+import { useChatSearchDirectory } from '@/hooks/useChatSearchDirectory';
+import type { ProjectFileSearchHit } from '@/lib/opencode/client';
+import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
+import { useDirectoryShowHidden } from '@/lib/directoryShowHidden';
+import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
+
+type FileInfo = ProjectFileSearchHit;
+type AgentInfo = {
+ name: string;
+ description?: string;
+ mode?: string | null;
+};
+
+export interface FileMentionHandle {
+ handleKeyDown: (key: string) => void;
+}
+
+type AutocompleteTab = 'commands' | 'agents' | 'files';
+
+interface FileMentionAutocompleteProps {
+ searchQuery: string;
+ onFileSelect: (file: FileInfo) => void;
+ onAgentSelect?: (agentName: string) => void;
+ onClose: () => void;
+ showTabs?: boolean;
+ activeTab?: AutocompleteTab;
+ onTabSelect?: (tab: AutocompleteTab) => void;
+ style?: React.CSSProperties;
+}
+
+export const FileMentionAutocomplete = React.forwardRef(({
+ searchQuery,
+ onFileSelect,
+ onAgentSelect,
+ onClose,
+ showTabs,
+ activeTab = 'files',
+ onTabSelect,
+ style,
+}, ref) => {
+ const currentDirectory = useChatSearchDirectory() ?? '';
+ const activeProjectId = useProjectsStore((state) => state.activeProjectId);
+ const activeProjectPath = useProjectsStore(
+ React.useCallback(
+ (state) => state.projects.find((project) => project.id === activeProjectId)?.path ?? null,
+ [activeProjectId],
+ ),
+ );
+ const projectRoot = React.useMemo(() => {
+ const candidate = activeProjectPath || currentDirectory;
+ return candidate ? candidate.replace(/\\/g, '/').replace(/\/+$/, '') : null;
+ }, [activeProjectPath, currentDirectory]);
+ const projectTabs = useFilesViewTabsStore(
+ React.useCallback(
+ (state) => (projectRoot ? state.byRoot[projectRoot] : undefined),
+ [projectRoot],
+ ),
+ );
+ const { getVisibleAgents } = useConfigStore();
+ const searchFiles = useFileSearchStore((state) => state.searchFiles);
+ const debouncedQuery = useDebouncedValue(searchQuery, 180);
+ const showHidden = useDirectoryShowHidden();
+ const showGitignored = useFilesViewShowGitignored();
+ const [files, setFiles] = React.useState([]);
+ const [agents, setAgents] = React.useState([]);
+ const [loading, setLoading] = React.useState(false);
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
+ const [marqueeWidth, setMarqueeWidth] = React.useState(360);
+ const [overflowMap, setOverflowMap] = React.useState>({});
+ const [marqueeDurations, setMarqueeDurations] = React.useState>({});
+ const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
+ const labelRefs = React.useRef<(HTMLSpanElement | null)[]>([]);
+ const measureRefs = React.useRef<(HTMLSpanElement | null)[]>([]);
+ const containerRef = React.useRef(null);
+ const ignoreTabClickRef = React.useRef(false);
+ const normalizedSearchQuery = (searchQuery ?? '').trim();
+ const visibleAgents = normalizedSearchQuery.length > 0 ? agents : agents.slice(0, 2);
+
+ const recentFiles = React.useMemo(() => {
+ if (!projectRoot || !projectTabs) {
+ return [] as FileInfo[];
+ }
+
+ const ordered = [
+ projectTabs.selectedPath,
+ ...projectTabs.openPaths.slice().reverse(),
+ ].filter((value): value is string => typeof value === 'string' && value.length > 0);
+
+ const seen = new Set();
+ const queryLower = normalizedSearchQuery.toLowerCase();
+ const mapped = ordered
+ .filter((filePath) => {
+ if (seen.has(filePath)) return false;
+ seen.add(filePath);
+ const relative = filePath.startsWith(`${projectRoot}/`) ? filePath.slice(projectRoot.length + 1) : filePath;
+ if (!queryLower) return true;
+ return relative.toLowerCase().includes(queryLower);
+ })
+ .slice(0, 6)
+ .map((filePath) => {
+ const normalizedPath = filePath.replace(/\\/g, '/');
+ const name = normalizedPath.split('/').filter(Boolean).pop() || normalizedPath;
+ const relativePath = normalizedPath.startsWith(`${projectRoot}/`)
+ ? normalizedPath.slice(projectRoot.length + 1)
+ : normalizedPath;
+ return {
+ name,
+ path: normalizedPath,
+ relativePath,
+ extension: name.includes('.') ? name.split('.').pop()?.toLowerCase() : undefined,
+ } satisfies FileInfo;
+ });
+
+ return mapped;
+ }, [normalizedSearchQuery, projectRoot, projectTabs]);
+
+ React.useEffect(() => {
+ const handlePointerDown = (event: MouseEvent | TouchEvent) => {
+ const target = event.target as Node | null;
+ if (!target || !containerRef.current) {
+ return;
+ }
+ if (containerRef.current.contains(target)) {
+ return;
+ }
+ onClose();
+ };
+
+ document.addEventListener('pointerdown', handlePointerDown, true);
+ return () => {
+ document.removeEventListener('pointerdown', handlePointerDown, true);
+ };
+ }, [onClose]);
+
+ React.useEffect(() => {
+ if (!currentDirectory) {
+ setFiles([]);
+ setLoading(false);
+ return;
+ }
+
+ const normalizedQuery = (debouncedQuery ?? '').trim();
+ const normalizedQueryLower = normalizedQuery
+ .replace(/^\.\//, '')
+ .replace(/^\/+/, '')
+ .toLowerCase();
+
+ if (!normalizedQueryLower) {
+ setFiles([]);
+ setLoading(false);
+ return;
+ }
+
+ let cancelled = false;
+ setLoading(true);
+
+ searchFiles(currentDirectory, normalizedQueryLower, 80, {
+ includeHidden: showHidden,
+ respectGitignore: !showGitignored,
+ type: 'file',
+ })
+ .then((hits) => {
+ if (cancelled) {
+ return;
+ }
+
+ const recentSet = new Set(recentFiles.map((file) => file.path));
+ setFiles(hits.filter((hit) => !recentSet.has(hit.path)).slice(0, 15));
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setFiles([]);
+ }
+ })
+ .finally(() => {
+ if (!cancelled) {
+ setLoading(false);
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [currentDirectory, debouncedQuery, recentFiles, searchFiles, showHidden, showGitignored]);
+
+ React.useEffect(() => {
+ const visibleAgents = getVisibleAgents();
+ const normalizedQuery = (searchQuery ?? '').trim().toLowerCase();
+ const filtered = visibleAgents
+ .filter((agent) => agent.mode && agent.mode !== 'primary')
+ .filter((agent) => {
+ if (!normalizedQuery) return true;
+ const haystack = `${agent.name} ${agent.description ?? ''}`.toLowerCase();
+ return haystack.includes(normalizedQuery);
+ })
+ .map((agent) => ({
+ name: agent.name,
+ description: agent.description,
+ mode: agent.mode,
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+ setAgents(filtered);
+ }, [getVisibleAgents, searchQuery]);
+
+ React.useEffect(() => {
+ setSelectedIndex(0);
+ setOverflowMap({});
+ setMarqueeDurations({});
+ }, [files, recentFiles.length, visibleAgents.length]);
+
+ React.useEffect(() => {
+ itemRefs.current[selectedIndex]?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest'
+ });
+ }, [selectedIndex]);
+
+ React.useEffect(() => {
+ let frameId: number | null = null;
+
+ const updateOverflow = () => {
+ if (frameId !== null) {
+ cancelAnimationFrame(frameId);
+ }
+ frameId = requestAnimationFrame(() => {
+ const next: Record = {};
+ const durations: Record = {};
+ labelRefs.current.forEach((node, index) => {
+ if (!node) {
+ return;
+ }
+ const measureNode = measureRefs.current[index];
+ const fullWidth = measureNode?.offsetWidth ?? node.scrollWidth;
+ const overflowPx = Math.max(0, fullWidth - node.clientWidth);
+ const isOverflowing = overflowPx > 8;
+ next[index] = isOverflowing;
+ if (isOverflowing) {
+ const duration = Math.max(0.6, overflowPx / 110);
+ durations[index] = duration;
+ }
+ });
+ setOverflowMap(next);
+ setMarqueeDurations(durations);
+ });
+ };
+
+ updateOverflow();
+ window.addEventListener('resize', updateOverflow);
+
+ return () => {
+ if (frameId !== null) {
+ cancelAnimationFrame(frameId);
+ }
+ window.removeEventListener('resize', updateOverflow);
+ };
+ }, [files]);
+
+ React.useEffect(() => {
+ const labelNode = labelRefs.current[selectedIndex];
+ if (!labelNode) {
+ return;
+ }
+
+ const updateWidth = () => {
+ const width = labelNode.clientWidth;
+ if (width > 0) {
+ setMarqueeWidth(width);
+ }
+ };
+
+ updateWidth();
+
+ if (typeof ResizeObserver === 'undefined') {
+ return;
+ }
+
+ const observer = new ResizeObserver(updateWidth);
+ observer.observe(labelNode);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [selectedIndex]);
+
+ const handleFileSelect = React.useCallback((file: FileInfo) => {
+ onFileSelect(file);
+ }, [onFileSelect]);
+
+ const handleAgentPick = React.useCallback((agentName: string) => {
+ onAgentSelect?.(agentName);
+ }, [onAgentSelect]);
+
+ React.useImperativeHandle(ref, () => ({
+ handleKeyDown: (key: string) => {
+ if (key === 'Escape') {
+ onClose();
+ return;
+ }
+
+ const total = visibleAgents.length + recentFiles.length + files.length;
+ if (total === 0) {
+ return;
+ }
+
+ if (key === 'ArrowDown') {
+ setSelectedIndex((prev) => (prev + 1) % total);
+ return;
+ }
+
+ if (key === 'ArrowUp') {
+ setSelectedIndex((prev) => (prev - 1 + total) % total);
+ return;
+ }
+
+ if (key === 'Enter' || key === 'Tab') {
+ const safeIndex = ((selectedIndex % total) + total) % total;
+ if (safeIndex < visibleAgents.length) {
+ const agent = visibleAgents[safeIndex];
+ if (agent) {
+ handleAgentPick(agent.name);
+ }
+ return;
+ }
+ const fileIndex = safeIndex - visibleAgents.length;
+ const selectedFile = fileIndex < recentFiles.length
+ ? recentFiles[fileIndex]
+ : files[fileIndex - recentFiles.length];
+ if (selectedFile) {
+ handleFileSelect(selectedFile);
+ }
+ }
+ }
+ }), [files, recentFiles, visibleAgents, selectedIndex, onClose, handleFileSelect, handleAgentPick]);
+
+ const getFileIcon = (file: FileInfo) => {
+ const ext = file.extension?.toLowerCase();
+ switch (ext) {
+ case 'ts':
+ case 'tsx':
+ case 'js':
+ case 'jsx':
+ return ;
+ case 'json':
+ return ;
+ case 'md':
+ case 'mdx':
+ return ;
+ case 'png':
+ case 'jpg':
+ case 'jpeg':
+ case 'gif':
+ case 'svg':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+ {showTabs ? (
+
+
+ {([
+ { id: 'commands' as const, label: 'Commands' },
+ { id: 'agents' as const, label: 'Agents' },
+ { id: 'files' as const, label: 'Files' },
+ ]).map((tab) => (
+ {
+ if (event.pointerType !== 'touch') {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ ignoreTabClickRef.current = true;
+ onTabSelect?.(tab.id);
+ }}
+ onClick={() => {
+ if (ignoreTabClickRef.current) {
+ ignoreTabClickRef.current = false;
+ return;
+ }
+ onTabSelect?.(tab.id);
+ }}
+ >
+ {tab.label}
+
+ ))}
+
+
+ ) : null}
+
+ {loading ? (
+
+
+
+ ) : (
+
+ {visibleAgents.map((agent, index) => {
+ const isSelected = selectedIndex === index;
+ return (
+
{ itemRefs.current[index] = el; }}
+ className={cn(
+ 'flex items-start gap-2 px-3 py-1.5 cursor-pointer typography-ui-label rounded-lg',
+ isSelected && 'bg-interactive-selection',
+ )}
+ onClick={() => handleAgentPick(agent.name)}
+ onMouseEnter={() => setSelectedIndex(index)}
+ >
+
+
@{agent.name}
+ {agent.description ? (
+
{agent.description}
+ ) : null}
+
+
+ );
+ })}
+ {visibleAgents.length === 2 && normalizedSearchQuery.length === 0 && agents.length > 2 && (
+
+ Type to search more agents
+
+ )}
+ {visibleAgents.length > 0 && (recentFiles.length > 0 || files.length > 0) && (
+
+ )}
+ {recentFiles.map((file, index) => {
+ const rowIndex = visibleAgents.length + index;
+ const relativePath = file.relativePath || file.name;
+ const displayPath = truncatePathMiddle(relativePath, { maxLength: 60 });
+ const isSelected = selectedIndex === rowIndex;
+ const isOverflowing = overflowMap[rowIndex] ?? false;
+ const marqueeDuration = marqueeDurations[rowIndex] ?? 2.6;
+
+ return (
+
{ itemRefs.current[rowIndex] = el; }}
+ className={cn(
+ "flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label rounded-lg",
+ isSelected && "bg-interactive-selection"
+ )}
+ onClick={() => handleFileSelect(file)}
+ onMouseEnter={() => setSelectedIndex(rowIndex)}
+ >
+ {getFileIcon(file)}
+ { labelRefs.current[rowIndex] = el; }}
+ className="relative flex-1 min-w-0 overflow-hidden file-mention-marquee-container"
+ style={isSelected ? {
+ ['--file-mention-marquee-width' as string]: `${marqueeWidth}px`,
+ ['--file-mention-marquee-duration' as string]: `${marqueeDuration}s`
+ } : undefined}
+ aria-label={relativePath}
+ >
+ { measureRefs.current[rowIndex] = el; }}
+ className="absolute invisible whitespace-nowrap pointer-events-none"
+ aria-hidden
+ >
+ {relativePath}
+
+ {isOverflowing && isSelected ? (
+
+ {relativePath}
+
+ ) : (
+
+ {displayPath}
+
+ )}
+
+
+ );
+ })}
+ {recentFiles.length > 0 && files.length > 0 && (
+
+ )}
+ {files.map((file, index) => {
+ const rowIndex = visibleAgents.length + recentFiles.length + index;
+ const relativePath = file.relativePath || file.name;
+ const displayPath = truncatePathMiddle(relativePath, { maxLength: 60 });
+ const isSelected = selectedIndex === rowIndex;
+ const isOverflowing = overflowMap[rowIndex] ?? false;
+ const marqueeDuration = marqueeDurations[rowIndex] ?? 2.6;
+
+ const item = (
+
{ itemRefs.current[rowIndex] = el; }}
+ className={cn(
+ "flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label rounded-lg",
+ isSelected && "bg-interactive-selection"
+ )}
+ onClick={() => handleFileSelect(file)}
+ onMouseEnter={() => setSelectedIndex(rowIndex)}
+ >
+ {getFileIcon(file)}
+ { labelRefs.current[rowIndex] = el; }}
+ className="relative flex-1 min-w-0 overflow-hidden file-mention-marquee-container"
+ style={isSelected ? {
+ ['--file-mention-marquee-width' as string]: `${marqueeWidth}px`,
+ ['--file-mention-marquee-duration' as string]: `${marqueeDuration}s`
+ } : undefined}
+ aria-label={relativePath}
+ >
+ { measureRefs.current[rowIndex] = el; }}
+ className="absolute invisible whitespace-nowrap pointer-events-none"
+ aria-hidden
+ >
+ {relativePath}
+
+ {isOverflowing && isSelected ? (
+
+ {relativePath}
+
+ ) : (
+
+ {displayPath}
+
+ )}
+
+
+ );
+
+ return (
+
+ {item}
+
+ );
+ })}
+ {files.length === 0 && recentFiles.length === 0 && visibleAgents.length === 0 && (
+
+ No matches found
+
+ )}
+
+ )}
+
+
+ ↑↓ navigate • Enter select • Esc close
+
+
+ );
+});
diff --git a/ui/src/components/chat/MarkdownRenderer.tsx b/ui/src/components/chat/MarkdownRenderer.tsx
new file mode 100644
index 0000000..cbc624a
--- /dev/null
+++ b/ui/src/components/chat/MarkdownRenderer.tsx
@@ -0,0 +1,1447 @@
+import React from 'react';
+import { Streamdown } from 'streamdown';
+import { createCodePlugin } from '@streamdown/code';
+import { renderMermaidASCII, renderMermaidSVG } from 'beautiful-mermaid';
+import 'streamdown/styles.css';
+import { FadeInOnReveal } from './message/FadeInOnReveal';
+import type { Part } from '@opencode-ai/sdk/v2';
+import { cn } from '@/lib/utils';
+import { RiFileCopyLine, RiCheckLine, RiDownloadLine } from '@remixicon/react';
+import { toast } from '@/components/ui';
+import { copyTextToClipboard } from '@/lib/clipboard';
+
+import { isVSCodeRuntime } from '@/lib/desktop';
+import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
+import { getStreamdownThemePair } from '@/lib/shiki/appThemeRegistry';
+import { getDefaultTheme } from '@/lib/theme/themes';
+import type { ToolPopupContent } from './message/types';
+import { useUIStore } from '@/stores/useUIStore';
+import { useDeviceInfo } from '@/lib/device';
+import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
+import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
+import type { EditorAPI } from '@/lib/api/types';
+
+const withStableStringId = (value: T, id: string): T => {
+ const existingPrimitive = (value as Record)[Symbol.toPrimitive];
+ if (typeof existingPrimitive === 'function') {
+ try {
+ if ((existingPrimitive as () => unknown)() === id) {
+ return value;
+ }
+ } catch {
+ // Ignore and attempt to define below.
+ }
+ }
+
+ try {
+ Object.defineProperty(value, 'toString', {
+ value: () => id,
+ enumerable: false,
+ configurable: true,
+ });
+ } catch {
+ // Ignore if non-configurable or frozen.
+ }
+
+ try {
+ Object.defineProperty(value, Symbol.toPrimitive, {
+ value: () => id,
+ enumerable: false,
+ configurable: true,
+ });
+ } catch {
+ // Ignore if non-configurable or frozen.
+ }
+
+ return value;
+};
+
+const useMarkdownShikiThemes = (): readonly [string | object, string | object] => {
+ const themeSystem = useOptionalThemeSystem();
+
+ const isVSCode = isVSCodeRuntime() && typeof window !== 'undefined';
+
+ const fallbackLight = getDefaultTheme(false);
+ const fallbackDark = getDefaultTheme(true);
+
+ const lightThemeId = themeSystem?.lightThemeId ?? fallbackLight.metadata.id;
+ const darkThemeId = themeSystem?.darkThemeId ?? fallbackDark.metadata.id;
+
+ const lightTheme =
+ themeSystem?.availableThemes.find((theme) => theme.metadata.id === lightThemeId) ??
+ fallbackLight;
+ const darkTheme =
+ themeSystem?.availableThemes.find((theme) => theme.metadata.id === darkThemeId) ??
+ fallbackDark;
+
+ const fallbackThemes = React.useMemo(
+ () => getStreamdownThemePair(lightTheme, darkTheme),
+ [darkTheme, lightTheme],
+ );
+
+ const getThemes = React.useCallback((): readonly [string | object, string | object] => {
+ if (!isVSCode) {
+ return fallbackThemes;
+ }
+
+ const provided = window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__;
+ if (provided?.light && provided?.dark) {
+ const light = withStableStringId(
+ { ...(provided.light as Record) },
+ `vscode-shiki-light:${String((provided.light as { name?: unknown })?.name ?? 'theme')}`,
+ );
+ const dark = withStableStringId(
+ { ...(provided.dark as Record) },
+ `vscode-shiki-dark:${String((provided.dark as { name?: unknown })?.name ?? 'theme')}`,
+ );
+ return [light, dark] as const;
+ }
+
+ return fallbackThemes;
+ }, [fallbackThemes, isVSCode]);
+
+ const [themes, setThemes] = React.useState(getThemes);
+
+ React.useEffect(() => {
+ if (!isVSCode) {
+ return;
+ }
+
+ setThemes(getThemes());
+ }, [getThemes, isVSCode]);
+
+ React.useEffect(() => {
+ if (!isVSCode) return;
+
+ const handler = (event: Event) => {
+ // Rely on the canonical `window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__` that the webview updates
+ // before dispatching this event, so we always apply stable cache keys and avoid stale token reuse.
+ void event;
+ setThemes(getThemes());
+ };
+
+ window.addEventListener('openchamber:vscode-shiki-themes', handler as EventListener);
+ return () => window.removeEventListener('openchamber:vscode-shiki-themes', handler as EventListener);
+ }, [getThemes, isVSCode]);
+
+ return isVSCode ? themes : fallbackThemes;
+};
+
+type StreamdownCodeThemes = NonNullable[0]>['themes'];
+
+const useStreamdownPlugins = (shikiThemes: readonly [string | object, string | object]) => {
+ return React.useMemo(
+ () => ({
+ code: createCodePlugin({
+ // Streamdown code plugin runtime accepts theme objects, but current type only models bundled theme names.
+ themes: shikiThemes as unknown as StreamdownCodeThemes,
+ }),
+ }),
+ [shikiThemes],
+ );
+};
+
+const useCurrentMermaidTheme = () => {
+ const themeSystem = useOptionalThemeSystem();
+ const fallbackLight = getDefaultTheme(false);
+ const fallbackDark = getDefaultTheme(true);
+
+ return themeSystem?.currentTheme
+ ?? (typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? fallbackDark
+ : fallbackLight);
+};
+
+// Table utility functions
+const extractTableData = (tableEl: HTMLTableElement): { headers: string[]; rows: string[][] } => {
+ const headers: string[] = [];
+ const rows: string[][] = [];
+
+ const thead = tableEl.querySelector('thead');
+ if (thead) {
+ const headerCells = thead.querySelectorAll('th');
+ headerCells.forEach(cell => headers.push(cell.innerText.trim()));
+ }
+
+ const tbody = tableEl.querySelector('tbody');
+ if (tbody) {
+ const rowEls = tbody.querySelectorAll('tr');
+ rowEls.forEach(row => {
+ const cells = row.querySelectorAll('td');
+ const rowData: string[] = [];
+ cells.forEach(cell => rowData.push(cell.innerText.trim()));
+ rows.push(rowData);
+ });
+ }
+
+ return { headers, rows };
+};
+
+const tableToCSV = ({ headers, rows }: { headers: string[]; rows: string[][] }): string => {
+ const escapeCell = (cell: string): string => {
+ if (cell.includes(',') || cell.includes('"') || cell.includes('\n')) {
+ return `"${cell.replace(/"/g, '""')}"`;
+ }
+ return cell;
+ };
+
+ const lines: string[] = [];
+ if (headers.length > 0) {
+ lines.push(headers.map(escapeCell).join(','));
+ }
+ rows.forEach(row => lines.push(row.map(escapeCell).join(',')));
+ return lines.join('\n');
+};
+
+const tableToTSV = ({ headers, rows }: { headers: string[]; rows: string[][] }): string => {
+ const escapeCell = (cell: string): string => {
+ return cell.replace(/\t/g, '\\t').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
+ };
+
+ const lines: string[] = [];
+ if (headers.length > 0) {
+ lines.push(headers.map(escapeCell).join('\t'));
+ }
+ rows.forEach(row => lines.push(row.map(escapeCell).join('\t')));
+ return lines.join('\n');
+};
+
+const tableToMarkdown = ({ headers, rows }: { headers: string[]; rows: string[][] }): string => {
+ if (headers.length === 0) return '';
+
+ const escapeCell = (cell: string): string => {
+ return cell.replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
+ };
+
+ const lines: string[] = [];
+ lines.push(`| ${headers.map(escapeCell).join(' | ')} |`);
+ lines.push(`| ${headers.map(() => '---').join(' | ')} |`);
+ rows.forEach(row => {
+ const paddedRow = headers.map((_, i) => escapeCell(row[i] || ''));
+ lines.push(`| ${paddedRow.join(' | ')} |`);
+ });
+ return lines.join('\n');
+};
+
+const downloadFile = (filename: string, content: string, mimeType: string) => {
+ const blob = new Blob([content], { type: mimeType });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+};
+
+// Table copy button with dropdown
+const TableCopyButton: React.FC<{ tableRef: React.RefObject }> = ({ tableRef }) => {
+ const [copied, setCopied] = React.useState(false);
+ const [showMenu, setShowMenu] = React.useState(false);
+ const menuRef = React.useRef(null);
+
+ React.useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ setShowMenu(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const handleCopy = async (format: 'csv' | 'tsv') => {
+ const tableEl = tableRef.current?.querySelector('table');
+ if (!tableEl) return;
+
+ const data = extractTableData(tableEl);
+ const content = format === 'csv' ? tableToCSV(data) : tableToTSV(data);
+
+ try {
+ await navigator.clipboard.write([
+ new ClipboardItem({
+ 'text/plain': new Blob([content], { type: 'text/plain' }),
+ 'text/html': new Blob([tableEl.outerHTML], { type: 'text/html' }),
+ }),
+ ]);
+ setCopied(true);
+ setShowMenu(false);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ const fallbackResult = await copyTextToClipboard(content);
+ if (fallbackResult.ok) {
+ setCopied(true);
+ setShowMenu(false);
+ setTimeout(() => setCopied(false), 2000);
+ return;
+ }
+ console.error('Failed to copy table:', err);
+ }
+ };
+
+ return (
+
+
setShowMenu(!showMenu)}
+ className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
+ title="Copy table"
+ >
+ {copied ? : }
+
+ {showMenu && (
+
+ handleCopy('csv')}
+ >
+ CSV
+
+ handleCopy('tsv')}
+ >
+ TSV
+
+
+ )}
+
+ );
+};
+
+// Table download button with dropdown
+const TableDownloadButton: React.FC<{ tableRef: React.RefObject }> = ({ tableRef }) => {
+ const [showMenu, setShowMenu] = React.useState(false);
+ const menuRef = React.useRef(null);
+
+ React.useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ setShowMenu(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const handleDownload = (format: 'csv' | 'markdown') => {
+ const tableEl = tableRef.current?.querySelector('table');
+ if (!tableEl) return;
+
+ const data = extractTableData(tableEl);
+ const content = format === 'csv' ? tableToCSV(data) : tableToMarkdown(data);
+ const filename = format === 'csv' ? 'table.csv' : 'table.md';
+ const mimeType = format === 'csv' ? 'text/csv' : 'text/markdown';
+ downloadFile(filename, content, mimeType);
+ setShowMenu(false);
+ toast.success(`Table downloaded as ${format.toUpperCase()}`);
+ };
+
+ return (
+
+
setShowMenu(!showMenu)}
+ className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
+ title="Download table"
+ >
+
+
+ {showMenu && (
+
+ handleDownload('csv')}
+ >
+ CSV
+
+ handleDownload('markdown')}
+ >
+ Markdown
+
+
+ )}
+
+ );
+};
+
+// Table wrapper with custom controls
+const TableWrapper: React.FC<{ children?: React.ReactNode; className?: string }> = ({ children, className }) => {
+ const tableRef = React.useRef(null);
+
+ return (
+
+ );
+};
+
+type CodeBlockWrapperProps = React.HTMLAttributes & {
+ children?: React.ReactNode;
+};
+
+const getMermaidInfo = (children: React.ReactNode): { isMermaid: boolean; source: string } => {
+ if (!React.isValidElement(children)) return { isMermaid: false, source: '' };
+ const props = children.props as Record | undefined;
+ const className = typeof props?.className === 'string' ? props.className : '';
+ if (!className.includes('language-mermaid')) return { isMermaid: false, source: '' };
+ // Extract raw mermaid source from the code element's children
+ const codeChildren = props?.children;
+ let source = '';
+ if (typeof codeChildren === 'string') {
+ source = codeChildren;
+ } else if (React.isValidElement(codeChildren)) {
+ const innerProps = codeChildren.props as Record | undefined;
+ if (typeof innerProps?.children === 'string') source = innerProps.children;
+ }
+ return { isMermaid: true, source };
+};
+
+const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ source, mode }) => {
+ const currentTheme = useCurrentMermaidTheme();
+ const { isMobile } = useDeviceInfo();
+ const [copied, setCopied] = React.useState(false);
+ const [downloaded, setDownloaded] = React.useState(false);
+
+ const svg = React.useMemo(() => {
+ if (mode !== 'svg') return '';
+ try {
+ return renderMermaidSVG(source, {
+ bg: currentTheme.colors.surface.elevated,
+ fg: currentTheme.colors.surface.foreground,
+ line: currentTheme.colors.interactive.border,
+ accent: currentTheme.colors.primary.base,
+ muted: currentTheme.colors.surface.mutedForeground,
+ surface: currentTheme.colors.surface.muted,
+ border: currentTheme.colors.interactive.border,
+ transparent: true,
+ font: 'IBM Plex Sans, sans-serif',
+ });
+ } catch {
+ return '';
+ }
+ }, [currentTheme, mode, source]);
+
+ const ascii = React.useMemo(() => {
+ if (mode !== 'ascii') return '';
+ try {
+ return renderMermaidASCII(source);
+ } catch {
+ return '';
+ }
+ }, [mode, source]);
+
+ const copyVisibilityClass = isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100';
+
+ const handleCopyAscii = async (asciiText: string) => {
+ if (!asciiText) return;
+ const result = await copyTextToClipboard(asciiText);
+ if (result.ok) {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ const handleCopyMermaidSource = async () => {
+ if (!source) return;
+ const result = await copyTextToClipboard(source);
+ if (result.ok) {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ const handleDownloadSvg = () => {
+ if (!svg) return;
+ try {
+ const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `diagram-${Date.now()}.svg`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ setDownloaded(true);
+ setTimeout(() => setDownloaded(false), 2000);
+ } catch {
+ toast.error('Failed to download diagram');
+ }
+ };
+
+ if (mode === 'ascii') {
+ const asciiText = ascii || source;
+
+ return (
+
+
+
+ handleCopyAscii(asciiText)}
+ className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
+ title="Copy"
+ >
+ {copied ? : }
+
+
+
+ );
+ }
+
+ if (!svg) {
+ return (
+
+
+
+ handleCopyAscii(source)}
+ className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
+ title="Copy"
+ >
+ {copied ? : }
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {copied ? : }
+
+
+ {downloaded ? : }
+
+
+
+ );
+};
+
+const CodeBlockWrapper: React.FC = ({ children, className, style, ...props }) => {
+ const mermaidInfo = getMermaidInfo(children);
+ const mermaidRenderingMode = useUIStore((state) => state.mermaidRenderingMode);
+ const codeChild = React.useMemo(
+ () => (
+ React.isValidElement(children)
+ ? React.cloneElement(children as React.ReactElement>, { 'data-block': true })
+ : children
+ ),
+ [children],
+ );
+
+ const normalizedStyle = React.useMemo(() => {
+ if (!style) return style;
+
+ const next: React.CSSProperties = { ...style };
+
+ const normalizeDeclarationString = (
+ raw: unknown
+ ): { value?: string; vars: Record } => {
+ if (typeof raw !== 'string') return { value: undefined, vars: {} };
+
+ const [valuePart, ...rest] = raw.split(';').map((p) => p.trim()).filter(Boolean);
+ const vars: Record = {};
+ for (const decl of rest) {
+ const idx = decl.indexOf(':');
+ if (idx === -1) continue;
+ const prop = decl.slice(0, idx).trim();
+ const value = decl.slice(idx + 1).trim();
+ if (!prop.startsWith('--') || value.length === 0) continue;
+ vars[prop] = value;
+ }
+ return { value: valuePart, vars };
+ };
+
+ const bg = normalizeDeclarationString((style as React.CSSProperties).backgroundColor);
+ if (bg.value) {
+ next.backgroundColor = bg.value;
+ }
+ for (const [k, v] of Object.entries(bg.vars)) {
+ (next as Record)[k] = v;
+ }
+
+ const fg = normalizeDeclarationString((style as React.CSSProperties).color);
+ if (fg.value) {
+ next.color = fg.value;
+ }
+ for (const [k, v] of Object.entries(fg.vars)) {
+ (next as Record)[k] = v;
+ }
+
+ return next;
+ }, [style]);
+
+ if (mermaidInfo.isMermaid) {
+ return ;
+ }
+
+ return (
+
+ {codeChild}
+
+ );
+};
+
+const streamdownComponents = {
+ pre: CodeBlockWrapper,
+ table: TableWrapper,
+};
+
+const streamdownControls = {
+ code: true,
+ table: false,
+};
+
+type MermaidControlOptions = {
+ download: boolean;
+ copy: boolean;
+ fullscreen: boolean;
+ panZoom: boolean;
+};
+
+const extractMermaidBlocks = (markdown: string): string[] => {
+ const blocks: string[] = [];
+ const regex = /(?:^|\r?\n)(`{3,}|~{3,})mermaid[^\n\r]*\r?\n([\s\S]*?)\r?\n\1(?=\r?\n|$)/gi;
+ let match: RegExpExecArray | null = regex.exec(markdown);
+
+ while (match) {
+ const block = (match[2] ?? '').replace(/\s+$/, '');
+ blocks.push(block);
+ match = regex.exec(markdown);
+ }
+
+ return blocks;
+};
+
+const stripLeadingFrontmatter = (markdown: string): string => {
+ const frontmatterMatch = markdown.match(
+ /^(?:\uFEFF)?(---|\+\+\+)[^\S\r\n]*\r?\n[\s\S]*?\r?\n\1[^\S\r\n]*(?:\r?\n|$)/,
+ );
+
+ if (!frontmatterMatch) {
+ return markdown;
+ }
+
+ return markdown.slice(frontmatterMatch[0].length);
+};
+
+export type MarkdownVariant = 'assistant' | 'tool';
+
+interface MarkdownRendererProps {
+ content: string;
+ part?: Part;
+ messageId: string;
+ isAnimated?: boolean;
+ className?: string;
+ isStreaming?: boolean;
+ variant?: MarkdownVariant;
+ onShowPopup?: (content: ToolPopupContent) => void;
+}
+
+const MERMAID_BLOCK_SELECTOR = '[data-streamdown="mermaid-block"]';
+const FILE_LINK_SELECTOR = '[data-openchamber-file-link="true"]';
+
+type ParsedFileReference = {
+ path: string;
+ line?: number;
+ column?: number;
+};
+
+const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
+const WINDOWS_UNC_PATH_PATTERN = /^\\\\[^\\]+\\[^\\]+/;
+const KNOWN_FILE_BASENAMES = new Set([
+ 'dockerfile',
+ 'makefile',
+ 'readme',
+ 'license',
+ '.env',
+ '.gitignore',
+ '.npmrc',
+]);
+const KNOWN_BASENAME_PATTERN = Array.from(KNOWN_FILE_BASENAMES)
+ .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
+ .join('|');
+
+const normalizePath = (value: string): string => {
+ const source = (value || '').trim();
+ if (!source) {
+ return '';
+ }
+
+ const withSlashes = source.replace(/\\/g, '/');
+ const hadUncPrefix = withSlashes.startsWith('//');
+
+ let normalized = withSlashes.replace(/\/+/g, '/');
+ if (hadUncPrefix && !normalized.startsWith('//')) {
+ normalized = `/${normalized}`;
+ }
+
+ const isUnixRoot = normalized === '/';
+ const isWindowsDriveRoot = /^[A-Za-z]:\/$/.test(normalized);
+ if (!isUnixRoot && !isWindowsDriveRoot) {
+ normalized = normalized.replace(/\/+$/, '');
+ }
+
+ return normalized;
+};
+
+const isAbsolutePath = (value: string): boolean => {
+ return value.startsWith('/')
+ || WINDOWS_DRIVE_PATH_PATTERN.test(value)
+ || WINDOWS_UNC_PATH_PATTERN.test(value)
+ || value.startsWith('//');
+};
+
+const toAbsolutePath = (basePath: string, targetPath: string): string => {
+ const normalizedTarget = normalizePath(targetPath);
+ if (!normalizedTarget) {
+ return normalizePath(basePath);
+ }
+
+ if (isAbsolutePath(normalizedTarget)) {
+ return normalizedTarget;
+ }
+
+ const normalizedBase = normalizePath(basePath);
+ if (!normalizedBase) {
+ return normalizedTarget;
+ }
+
+ const isWindowsDriveBase = /^[A-Za-z]:/.test(normalizedBase);
+ const prefix = isWindowsDriveBase ? normalizedBase.slice(0, 2) : '';
+ const baseRemainder = isWindowsDriveBase ? normalizedBase.slice(2) : normalizedBase;
+
+ const stack = baseRemainder.split('/').filter(Boolean);
+ const parts = normalizedTarget.split('/').filter(Boolean);
+ for (const part of parts) {
+ if (part === '.') {
+ continue;
+ }
+ if (part === '..') {
+ if (stack.length > 0) {
+ stack.pop();
+ }
+ continue;
+ }
+ stack.push(part);
+ }
+
+ if (isWindowsDriveBase) {
+ return `${prefix}/${stack.join('/')}`;
+ }
+
+ return `/${stack.join('/')}`;
+};
+
+const trimPathCandidate = (value: string): string => {
+ let next = (value || '').trim();
+ if (!next) {
+ return '';
+ }
+
+ if ((next.startsWith('`') && next.endsWith('`')) || (next.startsWith('"') && next.endsWith('"')) || (next.startsWith("'") && next.endsWith("'"))) {
+ next = next.slice(1, -1).trim();
+ }
+
+ next = next.replace(/[.,;!?]+$/g, '');
+
+ if (next.endsWith(')') && !next.includes('(')) {
+ next = next.slice(0, -1);
+ }
+ if (next.endsWith(']') && !next.includes('[')) {
+ next = next.slice(0, -1);
+ }
+
+ return next;
+};
+
+const stripTrailingReference = (value: string): string => {
+ let next = trimPathCandidate(value);
+ if (!next) {
+ return '';
+ }
+
+ const semicolonIndex = next.indexOf(';');
+ if (semicolonIndex >= 0) {
+ next = next.slice(0, semicolonIndex);
+ }
+
+ next = next.replace(/#.*$/, '');
+
+ const extensionSuffixMatch = next.match(/^(.*\.[A-Za-z0-9_-]{1,16}):.*$/);
+ if (extensionSuffixMatch) {
+ next = extensionSuffixMatch[1] ?? next;
+ }
+
+ const basenameSuffixMatch = KNOWN_BASENAME_PATTERN.length > 0
+ ? next.match(new RegExp(`^(.*(?:/|^)(${KNOWN_BASENAME_PATTERN})):.*$`, 'i'))
+ : null;
+ if (basenameSuffixMatch) {
+ next = basenameSuffixMatch[1] ?? next;
+ }
+
+ return trimPathCandidate(next);
+};
+
+const parseFileReference = (value: string): ParsedFileReference | null => {
+ const trimmed = trimPathCandidate(value);
+ if (!trimmed) {
+ return null;
+ }
+
+ const semicolonIndex = trimmed.indexOf(';');
+ const withoutSemicolonSuffix = semicolonIndex >= 0
+ ? trimPathCandidate(trimmed.slice(0, semicolonIndex))
+ : trimmed;
+ if (!withoutSemicolonSuffix) {
+ return null;
+ }
+
+ const hashMatch = withoutSemicolonSuffix.match(/^(.*)#L(\d+)(?:C(\d+))?$/i);
+ if (hashMatch) {
+ const path = stripTrailingReference(hashMatch[1] ?? '');
+ const line = Number.parseInt(hashMatch[2] ?? '', 10);
+ const column = hashMatch[3] ? Number.parseInt(hashMatch[3], 10) : undefined;
+ if (!path || !Number.isFinite(line)) {
+ return null;
+ }
+
+ return {
+ path,
+ line,
+ column: Number.isFinite(column ?? Number.NaN) ? column : undefined,
+ };
+ }
+
+ const colonMatch = withoutSemicolonSuffix.match(/^(.*):(\d+)(?::(\d+))?$/);
+ if (colonMatch) {
+ const path = stripTrailingReference(colonMatch[1] ?? '');
+ const line = Number.parseInt(colonMatch[2] ?? '', 10);
+ const column = colonMatch[3] ? Number.parseInt(colonMatch[3], 10) : undefined;
+ if (!path || !Number.isFinite(line)) {
+ return null;
+ }
+
+ return {
+ path,
+ line,
+ column: Number.isFinite(column ?? Number.NaN) ? column : undefined,
+ };
+ }
+
+ const pathOnly = stripTrailingReference(withoutSemicolonSuffix);
+ if (!pathOnly) {
+ return null;
+ }
+
+ return { path: pathOnly };
+};
+
+const hasFileExtension = (path: string): boolean => {
+ const base = path.split('/').filter(Boolean).pop() ?? '';
+ if (!base || base.endsWith('.')) {
+ return false;
+ }
+ return /\.[A-Za-z0-9_-]{1,16}$/.test(base);
+};
+
+const isLikelyFilePathValue = (path: string): boolean => {
+ if (!path || path.startsWith('--') || path.includes('://')) {
+ return false;
+ }
+
+ if (/[<>]/.test(path) || /\s{2,}/.test(path)) {
+ return false;
+ }
+
+ const normalized = normalizePath(path);
+ const baseName = normalized.split('/').filter(Boolean).pop() ?? normalized;
+ if (!baseName || baseName === '.' || baseName === '..') {
+ return false;
+ }
+
+ const base = baseName.toLowerCase();
+ if (KNOWN_FILE_BASENAMES.has(base) || (base.startsWith('.') && base.length > 1)) {
+ return true;
+ }
+
+ return hasFileExtension(normalized);
+};
+
+const isLikelyFilePath = (value: string): boolean => {
+ const parsed = parseFileReference(value);
+ if (!parsed) {
+ return false;
+ }
+ return isLikelyFilePathValue(parsed.path);
+};
+
+const extractPathCandidateFromElement = (element: HTMLElement): string => {
+ if (element.tagName.toLowerCase() === 'a') {
+ const href = element.getAttribute('href')?.trim();
+ if (href && isLikelyFilePath(href)) {
+ return href;
+ }
+ }
+
+ return (element.textContent || '').trim();
+};
+
+const getResolvedReference = (rawValue: string, effectiveDirectory: string): (ParsedFileReference & { resolvedPath: string }) | null => {
+ const parsed = parseFileReference(rawValue);
+ if (!parsed || !isLikelyFilePathValue(parsed.path)) {
+ return null;
+ }
+
+ const resolvedPath = isAbsolutePath(parsed.path)
+ ? normalizePath(parsed.path)
+ : toAbsolutePath(effectiveDirectory, parsed.path);
+ if (!resolvedPath) {
+ return null;
+ }
+
+ return {
+ ...parsed,
+ resolvedPath,
+ };
+};
+
+const getContextDirectory = (effectiveDirectory: string, resolvedPath: string): string => {
+ const normalizedDirectory = normalizePath(effectiveDirectory);
+ if (normalizedDirectory) {
+ return normalizedDirectory;
+ }
+
+ const normalizedPath = normalizePath(resolvedPath);
+ const parent = normalizedPath.replace(/\/[^/]*$/, '');
+ return parent || normalizedPath;
+};
+
+const useFileReferenceInteractions = ({
+ containerRef,
+ effectiveDirectory,
+ readFile,
+ editor,
+ preferRuntimeEditor,
+}: {
+ containerRef: React.RefObject;
+ effectiveDirectory: string;
+ readFile?: (path: string) => Promise<{ content: string; path: string }>;
+ editor?: EditorAPI;
+ preferRuntimeEditor?: boolean;
+}) => {
+ const validationCacheRef = React.useRef>(new Map());
+ const inFlightValidationsRef = React.useRef>>(new Map());
+ const annotationPassRef = React.useRef(0);
+ const annotationDebounceRef = React.useRef(null);
+ const isValidationSweepRunningRef = React.useRef(false);
+
+ React.useEffect(() => {
+ const container = containerRef.current;
+ if (!container) {
+ return;
+ }
+
+ let disposed = false;
+
+ const isPathResolvable = async (resolvedPath: string): Promise => {
+ const cache = validationCacheRef.current;
+ if (cache.has(resolvedPath)) {
+ return cache.get(resolvedPath) === true;
+ }
+
+ const inFlight = inFlightValidationsRef.current.get(resolvedPath);
+ if (inFlight) {
+ return inFlight;
+ }
+
+ const checkPromise = (async () => {
+ try {
+ if (!readFile) {
+ return false;
+ }
+ await readFile(resolvedPath);
+ cache.set(resolvedPath, true);
+ return true;
+ } catch {
+ cache.set(resolvedPath, false);
+ return false;
+ } finally {
+ inFlightValidationsRef.current.delete(resolvedPath);
+ }
+ })();
+
+ inFlightValidationsRef.current.set(resolvedPath, checkPromise);
+ return checkPromise;
+ };
+
+ const clearCandidateLinkAttrs = (candidate: HTMLElement) => {
+ candidate.removeAttribute('data-openchamber-file-link');
+ candidate.removeAttribute('data-openchamber-file-ref');
+ candidate.removeAttribute('data-openchamber-file-path');
+ if (candidate.getAttribute('title') === 'Open file') {
+ candidate.removeAttribute('title');
+ }
+ if (candidate.tagName.toLowerCase() !== 'a') {
+ candidate.removeAttribute('role');
+ candidate.removeAttribute('tabindex');
+ }
+ };
+
+ const applyCandidateLinkAttrs = (candidate: HTMLElement, rawCandidate: string, resolvedPath: string) => {
+ candidate.setAttribute('data-openchamber-file-link', 'true');
+ candidate.setAttribute('data-openchamber-file-ref', rawCandidate);
+ candidate.setAttribute('data-openchamber-file-path', resolvedPath);
+ candidate.setAttribute('title', 'Open file');
+ if (candidate.tagName.toLowerCase() !== 'a') {
+ candidate.setAttribute('role', 'button');
+ candidate.setAttribute('tabindex', '0');
+ }
+ };
+
+ const runValidationSweep = async (paths: string[], expectedPassID: number) => {
+ if (isValidationSweepRunningRef.current || paths.length === 0) {
+ return;
+ }
+
+ isValidationSweepRunningRef.current = true;
+ const maxConcurrent = 3;
+ let cursor = 0;
+
+ const worker = async () => {
+ while (!disposed && cursor < paths.length) {
+ const index = cursor;
+ cursor += 1;
+ const pathToCheck = paths[index];
+ if (!pathToCheck) {
+ continue;
+ }
+ await isPathResolvable(pathToCheck);
+ }
+ };
+
+ try {
+ await Promise.all(Array.from({ length: Math.min(maxConcurrent, paths.length) }, () => worker()));
+ } finally {
+ isValidationSweepRunningRef.current = false;
+ }
+
+ if (!disposed && annotationPassRef.current === expectedPassID) {
+ void annotateFileLinks();
+ }
+ };
+
+ const annotateFileLinks = async () => {
+ const passID = annotationPassRef.current + 1;
+ annotationPassRef.current = passID;
+ const candidates = container.querySelectorAll('[data-streamdown="inline-code"], a');
+ const unresolvedPaths = new Set();
+
+ for (const candidate of Array.from(candidates)) {
+ const rawCandidate = extractPathCandidateFromElement(candidate);
+ const resolved = getResolvedReference(rawCandidate, effectiveDirectory);
+ if (!resolved) {
+ clearCandidateLinkAttrs(candidate);
+ continue;
+ }
+
+ if (annotationPassRef.current !== passID) {
+ return;
+ }
+
+ const cachedResult = validationCacheRef.current.get(resolved.resolvedPath);
+ if (cachedResult === true) {
+ applyCandidateLinkAttrs(candidate, rawCandidate, resolved.resolvedPath);
+ continue;
+ }
+
+ clearCandidateLinkAttrs(candidate);
+ if (cachedResult !== false) {
+ unresolvedPaths.add(resolved.resolvedPath);
+ }
+ }
+
+ if (unresolvedPaths.size > 0) {
+ void runValidationSweep(Array.from(unresolvedPaths), passID);
+ }
+ };
+
+ const openFileReference = async (sourceElement: HTMLElement): Promise => {
+ const raw = sourceElement.getAttribute('data-openchamber-file-ref') || extractPathCandidateFromElement(sourceElement);
+ const resolved = getResolvedReference(raw, effectiveDirectory);
+ if (!resolved) {
+ return false;
+ }
+
+ const isResolvable = await isPathResolvable(resolved.resolvedPath);
+ if (!isResolvable) {
+ sourceElement.removeAttribute('data-openchamber-file-link');
+ sourceElement.removeAttribute('data-openchamber-file-ref');
+ sourceElement.removeAttribute('data-openchamber-file-path');
+ if (sourceElement.getAttribute('title') === 'Open file') {
+ sourceElement.removeAttribute('title');
+ }
+ return false;
+ }
+
+ const contextDirectory = getContextDirectory(effectiveDirectory, resolved.resolvedPath);
+ if (preferRuntimeEditor && editor) {
+ await editor.openFile(
+ resolved.resolvedPath,
+ Number.isFinite(resolved.line ?? Number.NaN)
+ ? Math.max(1, Math.trunc(resolved.line as number))
+ : undefined,
+ Number.isFinite(resolved.column ?? Number.NaN)
+ ? Math.max(1, Math.trunc(resolved.column as number))
+ : undefined,
+ );
+ return true;
+ }
+
+ const uiStore = useUIStore.getState();
+ if (Number.isFinite(resolved.line ?? Number.NaN)) {
+ uiStore.openContextFileAtLine(
+ contextDirectory,
+ resolved.resolvedPath,
+ Math.max(1, Math.trunc(resolved.line as number)),
+ Number.isFinite(resolved.column ?? Number.NaN)
+ ? Math.max(1, Math.trunc(resolved.column as number))
+ : 1,
+ );
+ } else {
+ uiStore.openContextFile(contextDirectory, resolved.resolvedPath);
+ }
+ return true;
+ };
+
+ const handleClick = (event: MouseEvent) => {
+ const target = event.target;
+ if (!(target instanceof Element)) {
+ return;
+ }
+
+ const fileRefElement = target.closest(FILE_LINK_SELECTOR);
+ if (!(fileRefElement instanceof HTMLElement)) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ void openFileReference(fileRefElement);
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key !== 'Enter' && event.key !== ' ') {
+ return;
+ }
+
+ const target = event.target;
+ if (!(target instanceof HTMLElement) || target.getAttribute('data-openchamber-file-link') !== 'true') {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ void openFileReference(target);
+ };
+
+ void annotateFileLinks();
+
+ const observer = new MutationObserver(() => {
+ if (annotationDebounceRef.current !== null && typeof window !== 'undefined') {
+ window.clearTimeout(annotationDebounceRef.current);
+ }
+ if (typeof window === 'undefined') {
+ void annotateFileLinks();
+ return;
+ }
+ annotationDebounceRef.current = window.setTimeout(() => {
+ annotationDebounceRef.current = null;
+ void annotateFileLinks();
+ }, 120);
+ });
+ observer.observe(container, { childList: true, subtree: true, characterData: true });
+
+ container.addEventListener('click', handleClick);
+ container.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ disposed = true;
+ annotationPassRef.current += 1;
+ if (annotationDebounceRef.current !== null && typeof window !== 'undefined') {
+ window.clearTimeout(annotationDebounceRef.current);
+ }
+ annotationDebounceRef.current = null;
+ observer.disconnect();
+ container.removeEventListener('click', handleClick);
+ container.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [containerRef, editor, effectiveDirectory, preferRuntimeEditor, readFile]);
+};
+
+const useMermaidInlineInteractions = ({
+ containerRef,
+ mermaidBlocks,
+ onShowPopup,
+ allowWheelZoom,
+}: {
+ containerRef: React.RefObject;
+ mermaidBlocks: string[];
+ onShowPopup?: (content: ToolPopupContent) => void;
+ allowWheelZoom?: boolean;
+}) => {
+ React.useEffect(() => {
+ const container = containerRef.current;
+ if (!container) {
+ return;
+ }
+
+ const handleMermaidClick = (event: MouseEvent) => {
+ if (!onShowPopup) {
+ return;
+ }
+
+ const target = event.target;
+ if (!(target instanceof Element)) {
+ return;
+ }
+
+ if (target.closest('button, a, [role="button"]')) {
+ return;
+ }
+
+ const block = target.closest(MERMAID_BLOCK_SELECTOR);
+ if (!block) {
+ return;
+ }
+
+ const renderedBlocks = Array.from(container.querySelectorAll(MERMAID_BLOCK_SELECTOR));
+ const blockIndex = renderedBlocks.indexOf(block);
+ if (blockIndex < 0) {
+ return;
+ }
+
+ const source = mermaidBlocks[blockIndex];
+ if (!source || source.trim().length === 0) {
+ return;
+ }
+
+ const filename = `Diagram ${blockIndex + 1}`;
+ onShowPopup({
+ open: true,
+ title: filename,
+ content: '',
+ metadata: {
+ tool: 'mermaid-preview',
+ filename,
+ },
+ mermaid: {
+ url: `data:text/plain;charset=utf-8,${encodeURIComponent(source)}`,
+ source,
+ filename,
+ },
+ });
+ };
+
+ const handleInlineWheel = (event: WheelEvent) => {
+ if (allowWheelZoom) {
+ return;
+ }
+
+ const target = event.target;
+ if (!(target instanceof Element)) {
+ return;
+ }
+
+ const block = target.closest(MERMAID_BLOCK_SELECTOR);
+ if (!block) {
+ return;
+ }
+
+ // Keep regular page scroll while preventing Streamdown inline wheel-zoom handlers.
+ event.stopPropagation();
+ };
+
+ container.addEventListener('click', handleMermaidClick);
+ container.addEventListener('wheel', handleInlineWheel, { capture: true, passive: true });
+
+ return () => {
+ container.removeEventListener('click', handleMermaidClick);
+ container.removeEventListener('wheel', handleInlineWheel, true);
+ };
+ }, [allowWheelZoom, containerRef, mermaidBlocks, onShowPopup]);
+};
+
+export const MarkdownRenderer: React.FC = ({
+ content,
+ part,
+ messageId,
+ isAnimated = true,
+ className,
+ isStreaming = false,
+ variant = 'assistant',
+ onShowPopup,
+}) => {
+ const { files, editor, runtime } = useRuntimeAPIs();
+ const streamdownContainerRef = React.useRef(null);
+ const effectiveDirectory = useEffectiveDirectory() ?? '';
+ const mermaidBlocks = React.useMemo(() => extractMermaidBlocks(content), [content]);
+ useMermaidInlineInteractions({ containerRef: streamdownContainerRef, mermaidBlocks, onShowPopup });
+ useFileReferenceInteractions({
+ containerRef: streamdownContainerRef,
+ effectiveDirectory,
+ readFile: files.readFile,
+ editor,
+ preferRuntimeEditor: runtime.isVSCode,
+ });
+
+ const shikiThemes = useMarkdownShikiThemes();
+ const streamdownPlugins = useStreamdownPlugins(shikiThemes);
+ const currentMermaidTheme = useCurrentMermaidTheme();
+ const componentKey = `markdown-${part?.id ? `part-${part.id}` : `message-${messageId}`}`;
+
+ const streamdownClassName = variant === 'tool'
+ ? 'streamdown-content streamdown-tool'
+ : 'streamdown-content';
+
+ const markdownContent = (
+
+
+ {content}
+
+
+ );
+
+ if (isAnimated) {
+ return (
+
+ {markdownContent}
+
+ );
+ }
+
+ return markdownContent;
+};
+
+export const SimpleMarkdownRenderer: React.FC<{
+ content: string;
+ className?: string;
+ variant?: MarkdownVariant;
+ disableLinkSafety?: boolean;
+ stripFrontmatter?: boolean;
+ onShowPopup?: (content: ToolPopupContent) => void;
+ mermaidControls?: MermaidControlOptions;
+ allowMermaidWheelZoom?: boolean;
+}> = ({
+ content,
+ className,
+ variant = 'assistant',
+ disableLinkSafety,
+ stripFrontmatter = false,
+ onShowPopup,
+ allowMermaidWheelZoom = false,
+}) => {
+ const { files, editor, runtime } = useRuntimeAPIs();
+ const renderedContent = React.useMemo(
+ () => (stripFrontmatter ? stripLeadingFrontmatter(content) : content),
+ [content, stripFrontmatter],
+ );
+ const streamdownContainerRef = React.useRef(null);
+ const effectiveDirectory = useEffectiveDirectory() ?? '';
+ const mermaidBlocks = React.useMemo(() => extractMermaidBlocks(renderedContent), [renderedContent]);
+ useMermaidInlineInteractions({
+ containerRef: streamdownContainerRef,
+ mermaidBlocks,
+ onShowPopup,
+ allowWheelZoom: allowMermaidWheelZoom,
+ });
+ useFileReferenceInteractions({
+ containerRef: streamdownContainerRef,
+ effectiveDirectory,
+ readFile: files.readFile,
+ editor,
+ preferRuntimeEditor: runtime.isVSCode,
+ });
+
+ const shikiThemes = useMarkdownShikiThemes();
+ const streamdownPlugins = useStreamdownPlugins(shikiThemes);
+ const currentMermaidTheme = useCurrentMermaidTheme();
+
+ const streamdownClassName = variant === 'tool'
+ ? 'streamdown-content streamdown-tool'
+ : 'streamdown-content';
+
+ return (
+
+
+ {renderedContent}
+
+
+ );
+};
diff --git a/ui/src/components/chat/MessageList.tsx b/ui/src/components/chat/MessageList.tsx
new file mode 100644
index 0000000..deb48e8
--- /dev/null
+++ b/ui/src/components/chat/MessageList.tsx
@@ -0,0 +1,1075 @@
+import React from 'react';
+import type { Message, Part } from '@opencode-ai/sdk/v2';
+import { flushSync } from 'react-dom';
+import { elementScroll, observeElementOffset, observeElementRect, Virtualizer } from '@tanstack/react-virtual';
+import { useShallow } from 'zustand/react/shallow';
+import type { ReactVirtualizerOptions, VirtualItem } from '@tanstack/react-virtual';
+
+import ChatMessage from './ChatMessage';
+import { PermissionCard } from './PermissionCard';
+import { QuestionCard } from './QuestionCard';
+import type { PermissionRequest } from '@/types/permission';
+import type { QuestionRequest } from '@/types/question';
+import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatScrollManager';
+import { filterSyntheticParts } from '@/lib/messages/synthetic';
+import { detectTurns, type Turn } from './hooks/useTurnGrouping';
+import { TurnGroupingProvider, useMessageNeighbors, useTurnGroupingContextForMessage, useTurnGroupingContextStatic } from './contexts/TurnGroupingContext';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { useDeviceInfo } from '@/lib/device';
+import { FadeInDisabledProvider } from './message/FadeInOnReveal';
+
+const MESSAGE_VIRTUALIZE_THRESHOLD = 40;
+const MESSAGE_VIRTUAL_OVERSCAN_MOBILE = 2;
+const MESSAGE_VIRTUAL_OVERSCAN_DESKTOP = 4;
+
+type MessageListVirtualizerOptions = Omit<
+ ReactVirtualizerOptions,
+ 'scrollToFn' | 'observeElementRect' | 'observeElementOffset'
+>
+
+const useMessageListVirtualizer = (
+ options: MessageListVirtualizerOptions,
+): Virtualizer => {
+ const [, forceRender] = React.useReducer(() => ({}), {});
+ const { useFlushSync = true, onChange, ...baseOptions } = options;
+
+ const handleChange = React.useCallback((instance: Virtualizer, sync: boolean) => {
+ if (useFlushSync && sync) {
+ flushSync(forceRender);
+ } else {
+ forceRender();
+ }
+
+ onChange?.(instance, sync);
+ }, [onChange, useFlushSync]);
+
+ const [virtualizer] = React.useState(() => new Virtualizer({
+ ...baseOptions,
+ onChange: handleChange,
+ observeElementRect,
+ observeElementOffset,
+ scrollToFn: elementScroll,
+ }));
+
+ virtualizer.setOptions({
+ ...baseOptions,
+ onChange: handleChange,
+ observeElementRect,
+ observeElementOffset,
+ scrollToFn: elementScroll,
+ });
+
+ React.useLayoutEffect(() => virtualizer._didMount(), [virtualizer]);
+ React.useLayoutEffect(() => virtualizer._willUpdate(), [virtualizer]);
+
+ return virtualizer;
+};
+
+interface ChatMessageEntry {
+ info: Message;
+ parts: Part[];
+}
+
+const USER_SHELL_MARKER = 'The following tool was executed by the user';
+
+const resolveMessageRole = (message: ChatMessageEntry): string | null => {
+ const info = message.info as unknown as { clientRole?: string | null | undefined; role?: string | null | undefined };
+ return (typeof info.clientRole === 'string' ? info.clientRole : null)
+ ?? (typeof info.role === 'string' ? info.role : null)
+ ?? null;
+};
+
+const isUserSubtaskMessage = (message: ChatMessageEntry | undefined): boolean => {
+ if (!message) return false;
+ if (resolveMessageRole(message) !== 'user') return false;
+ return message.parts.some((part) => part?.type === 'subtask');
+};
+
+const getMessageId = (message: ChatMessageEntry | undefined): string | null => {
+ if (!message) return null;
+ const id = (message.info as unknown as { id?: unknown }).id;
+ return typeof id === 'string' && id.trim().length > 0 ? id : null;
+};
+
+const getMessageParentId = (message: ChatMessageEntry): string | null => {
+ const parentID = (message.info as unknown as { parentID?: unknown }).parentID;
+ return typeof parentID === 'string' && parentID.trim().length > 0 ? parentID : null;
+};
+
+const isUserShellMarkerMessage = (message: ChatMessageEntry | undefined): boolean => {
+ if (!message) return false;
+ if (resolveMessageRole(message) !== 'user') return false;
+
+ return message.parts.some((part) => {
+ if (part?.type !== 'text') return false;
+ const text = (part as unknown as { text?: unknown }).text;
+ const synthetic = (part as unknown as { synthetic?: unknown }).synthetic;
+ return synthetic === true && typeof text === 'string' && text.trim().startsWith(USER_SHELL_MARKER);
+ });
+};
+
+type ShellBridgeDetails = {
+ command?: string;
+ output?: string;
+ status?: string;
+};
+
+const getShellBridgeAssistantDetails = (message: ChatMessageEntry, expectedParentId: string | null): { hide: boolean; details: ShellBridgeDetails | null } => {
+ if (resolveMessageRole(message) !== 'assistant') {
+ return { hide: false, details: null };
+ }
+
+ if (expectedParentId && getMessageParentId(message) !== expectedParentId) {
+ return { hide: false, details: null };
+ }
+
+ if (message.parts.length !== 1) {
+ return { hide: false, details: null };
+ }
+
+ const part = message.parts[0] as unknown as {
+ type?: unknown;
+ tool?: unknown;
+ state?: {
+ status?: unknown;
+ input?: { command?: unknown };
+ output?: unknown;
+ metadata?: { output?: unknown };
+ };
+ };
+
+ if (part.type !== 'tool') {
+ return { hide: false, details: null };
+ }
+
+ const toolName = typeof part.tool === 'string' ? part.tool.toLowerCase() : '';
+ if (toolName !== 'bash') {
+ return { hide: false, details: null };
+ }
+
+ const command = typeof part.state?.input?.command === 'string' ? part.state.input.command : undefined;
+ const output =
+ (typeof part.state?.output === 'string' ? part.state.output : undefined)
+ ?? (typeof part.state?.metadata?.output === 'string' ? part.state.metadata.output : undefined);
+ const status = typeof part.state?.status === 'string' ? part.state.status : undefined;
+
+ return {
+ hide: true,
+ details: {
+ command,
+ output,
+ status,
+ },
+ };
+};
+
+const readTaskSessionId = (toolPart: Part): string | null => {
+ const partRecord = toolPart as unknown as {
+ state?: {
+ metadata?: { sessionId?: unknown; sessionID?: unknown };
+ output?: unknown;
+ };
+ };
+ const metadata = partRecord.state?.metadata;
+ const fromMetadata =
+ (typeof metadata?.sessionId === 'string' && metadata.sessionId.trim().length > 0
+ ? metadata.sessionId.trim()
+ : null)
+ ?? (typeof metadata?.sessionID === 'string' && metadata.sessionID.trim().length > 0
+ ? metadata.sessionID.trim()
+ : null);
+ if (fromMetadata) return fromMetadata;
+
+ const output = partRecord.state?.output;
+ if (typeof output === 'string') {
+ const match = output.match(/task_id:\s*([a-zA-Z0-9_]+)/);
+ if (match?.[1]) {
+ return match[1];
+ }
+ }
+
+ return null;
+};
+
+const isSyntheticSubtaskBridgeAssistant = (message: ChatMessageEntry): { hide: boolean; taskSessionId: string | null } => {
+ if (resolveMessageRole(message) !== 'assistant') {
+ return { hide: false, taskSessionId: null };
+ }
+
+ if (message.parts.length !== 1) {
+ return { hide: false, taskSessionId: null };
+ }
+
+ const onlyPart = message.parts[0] as unknown as {
+ type?: unknown;
+ tool?: unknown;
+ };
+
+ if (onlyPart.type !== 'tool') {
+ return { hide: false, taskSessionId: null };
+ }
+
+ const toolName = typeof onlyPart.tool === 'string' ? onlyPart.tool.toLowerCase() : '';
+ if (toolName !== 'task') {
+ return { hide: false, taskSessionId: null };
+ }
+
+ return {
+ hide: true,
+ taskSessionId: readTaskSessionId(message.parts[0]),
+ };
+};
+
+const withSubtaskSessionId = (message: ChatMessageEntry, taskSessionId: string | null): ChatMessageEntry => {
+ if (!taskSessionId) return message;
+ const nextParts = message.parts.map((part) => {
+ if (part?.type !== 'subtask') return part;
+ const existing = (part as unknown as { taskSessionID?: unknown }).taskSessionID;
+ if (typeof existing === 'string' && existing.trim().length > 0) return part;
+ return {
+ ...part,
+ taskSessionID: taskSessionId,
+ } as Part;
+ });
+
+ return {
+ ...message,
+ parts: nextParts,
+ };
+};
+
+const withShellBridgeDetails = (message: ChatMessageEntry, details: ShellBridgeDetails | null): ChatMessageEntry => {
+ const command = typeof details?.command === 'string' ? details.command.trim() : '';
+ const output = typeof details?.output === 'string' ? details.output : '';
+ const status = typeof details?.status === 'string' ? details.status.trim() : '';
+
+ const nextParts: Part[] = [];
+ let injected = false;
+
+ for (const part of message.parts) {
+ if (!injected && part?.type === 'text') {
+ const text = (part as unknown as { text?: unknown }).text;
+ const synthetic = (part as unknown as { synthetic?: unknown }).synthetic;
+ if (synthetic === true && typeof text === 'string' && text.trim().startsWith(USER_SHELL_MARKER)) {
+ nextParts.push({
+ type: 'text',
+ text: '/shell',
+ shellAction: {
+ ...(command ? { command } : {}),
+ ...(output ? { output } : {}),
+ ...(status ? { status } : {}),
+ },
+ } as unknown as Part);
+ injected = true;
+ continue;
+ }
+ }
+ nextParts.push(part);
+ }
+
+ if (!injected) {
+ nextParts.push({
+ type: 'text',
+ text: '/shell',
+ shellAction: {
+ ...(command ? { command } : {}),
+ ...(output ? { output } : {}),
+ ...(status ? { status } : {}),
+ },
+ } as unknown as Part);
+ }
+
+ return {
+ ...message,
+ parts: nextParts,
+ };
+};
+
+interface MessageListProps {
+ messages: ChatMessageEntry[];
+ permissions: PermissionRequest[];
+ questions: QuestionRequest[];
+ onMessageContentChange: (reason?: ContentChangeReason) => void;
+ getAnimationHandlers: (messageId: string) => AnimationHandlers;
+ hasMoreAbove: boolean;
+ isLoadingOlder: boolean;
+ onLoadOlder: () => void;
+ hasRenderEarlier?: boolean;
+ onRenderEarlier?: () => void;
+ scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
+ scrollRef?: React.RefObject;
+}
+
+export interface MessageListHandle {
+ scrollToMessageId: (messageId: string, options?: { behavior?: ScrollBehavior }) => boolean;
+ captureViewportAnchor: () => { messageId: string; offsetTop: number } | null;
+ restoreViewportAnchor: (anchor: { messageId: string; offsetTop: number }) => boolean;
+}
+
+type RenderEntry =
+ | { kind: 'ungrouped'; key: string; message: ChatMessageEntry; isInLastTurn: boolean }
+ | { kind: 'turn'; key: string; turn: Turn; isLastTurn: boolean };
+
+interface MessageRowProps {
+ message: ChatMessageEntry;
+ onContentChange: (reason?: ContentChangeReason) => void;
+ animationHandlers: AnimationHandlers;
+ scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
+}
+
+// Static MessageRow - does NOT subscribe to dynamic context
+// Used for messages NOT in the last turn - no re-renders during streaming
+const StaticMessageRow = React.memo(({
+ message,
+ onContentChange,
+ animationHandlers,
+ scrollToBottom,
+}) => {
+ const { previousMessage, nextMessage } = useMessageNeighbors(message.info.id);
+ const turnGroupingContext = useTurnGroupingContextStatic(message.info.id);
+
+ return (
+
+ );
+});
+
+StaticMessageRow.displayName = 'StaticMessageRow';
+
+// Dynamic MessageRow - subscribes to dynamic context for streaming state
+// Used for messages in the LAST turn only
+const DynamicMessageRow = React.memo(({
+ message,
+ onContentChange,
+ animationHandlers,
+ scrollToBottom,
+}) => {
+ const { previousMessage, nextMessage } = useMessageNeighbors(message.info.id);
+ const turnGroupingContext = useTurnGroupingContextForMessage(message.info.id);
+
+ return (
+
+ );
+});
+
+DynamicMessageRow.displayName = 'DynamicMessageRow';
+
+interface TurnBlockProps {
+ turn: Turn;
+ isLastTurn: boolean;
+ onMessageContentChange: (reason?: ContentChangeReason) => void;
+ getAnimationHandlers: (messageId: string) => AnimationHandlers;
+ scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
+ stickyUserHeader?: boolean;
+}
+
+const TurnBlock: React.FC = ({
+ turn,
+ isLastTurn,
+ onMessageContentChange,
+ getAnimationHandlers,
+ scrollToBottom,
+ stickyUserHeader = true,
+}) => {
+ const renderMessage = React.useCallback(
+ (message: ChatMessageEntry) => {
+ const role = (message.info as { clientRole?: string | null | undefined }).clientRole ?? message.info.role;
+ const isInLastTurn = role !== 'user' && isLastTurn;
+ const RowComponent = isInLastTurn ? DynamicMessageRow : StaticMessageRow;
+
+ return (
+
+ );
+ },
+ [getAnimationHandlers, isLastTurn, onMessageContentChange, scrollToBottom]
+ );
+
+ return (
+
+ {stickyUserHeader ? (
+
+
+ {renderMessage(turn.userMessage)}
+
+
+
+ ) : (
+ renderMessage(turn.userMessage)
+ )}
+
+
+ {turn.assistantMessages.map((message) => renderMessage(message))}
+
+
+ );
+};
+
+TurnBlock.displayName = 'TurnBlock';
+
+interface UngroupedMessageRowProps {
+ message: ChatMessageEntry;
+ isInLastTurn: boolean;
+ onMessageContentChange: (reason?: ContentChangeReason) => void;
+ getAnimationHandlers: (messageId: string) => AnimationHandlers;
+ scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
+}
+
+const UngroupedMessageRow: React.FC = React.memo(({
+ message,
+ isInLastTurn,
+ onMessageContentChange,
+ getAnimationHandlers,
+ scrollToBottom,
+}) => {
+ const RowComponent = isInLastTurn ? DynamicMessageRow : StaticMessageRow;
+
+ return (
+
+ );
+});
+
+UngroupedMessageRow.displayName = 'UngroupedMessageRow';
+
+interface MessageListEntryProps {
+ entry: RenderEntry;
+ onMessageContentChange: (reason?: ContentChangeReason) => void;
+ getAnimationHandlers: (messageId: string) => AnimationHandlers;
+ scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
+ stickyUserHeader?: boolean;
+}
+
+const MessageListEntry: React.FC = React.memo(({
+ entry,
+ onMessageContentChange,
+ getAnimationHandlers,
+ scrollToBottom,
+ stickyUserHeader,
+}) => {
+ if (entry.kind === 'ungrouped') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}, areMessageListEntryPropsEqual);
+
+MessageListEntry.displayName = 'MessageListEntry';
+
+function areMessageListEntryPropsEqual(prevProps: MessageListEntryProps, nextProps: MessageListEntryProps): boolean {
+ if (prevProps.stickyUserHeader !== nextProps.stickyUserHeader) return false;
+ if (prevProps.onMessageContentChange !== nextProps.onMessageContentChange) return false;
+ if (prevProps.getAnimationHandlers !== nextProps.getAnimationHandlers) return false;
+ if (prevProps.scrollToBottom !== nextProps.scrollToBottom) return false;
+
+ const prevEntry = prevProps.entry;
+ const nextEntry = nextProps.entry;
+ if (prevEntry.kind !== nextEntry.kind) return false;
+ if (prevEntry.key !== nextEntry.key) return false;
+
+ if (prevEntry.kind === 'turn' && nextEntry.kind === 'turn') {
+ return prevEntry.turn === nextEntry.turn && prevEntry.isLastTurn === nextEntry.isLastTurn;
+ }
+
+ if (prevEntry.kind === 'ungrouped' && nextEntry.kind === 'ungrouped') {
+ return prevEntry.message === nextEntry.message && prevEntry.isInLastTurn === nextEntry.isInLastTurn;
+ }
+
+ return false;
+}
+
+// Inner component that renders messages with access to context hooks
+const MessageListContent: React.FC<{
+ entries: RenderEntry[];
+ onMessageContentChange: (reason?: ContentChangeReason) => void;
+ getAnimationHandlers: (messageId: string) => AnimationHandlers;
+ scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
+ stickyUserHeader: boolean;
+}> = ({ entries, onMessageContentChange, getAnimationHandlers, scrollToBottom, stickyUserHeader }) => {
+ return (
+ <>
+ {entries.map((entry) => (
+
+ ))}
+ >
+ );
+};
+
+const MessageList = React.forwardRef(({
+ messages,
+ permissions,
+ questions,
+ onMessageContentChange,
+ getAnimationHandlers,
+ hasMoreAbove,
+ isLoadingOlder,
+ onLoadOlder,
+ hasRenderEarlier,
+ onRenderEarlier,
+ scrollToBottom,
+ scrollRef,
+}, ref) => {
+ const { isMobile } = useDeviceInfo();
+ const stickyUserHeader = useUIStore(state => state.stickyUserHeader);
+
+ React.useEffect(() => {
+ if (permissions.length === 0 && questions.length === 0) {
+ return;
+ }
+ onMessageContentChange('permission');
+ }, [permissions, questions, onMessageContentChange]);
+
+ const baseDisplayMessages = React.useMemo(() => {
+ const seenIdsFromTail = new Set();
+
+ const dedupedMessages: ChatMessageEntry[] = [];
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
+ const message = messages[index];
+ const messageId = message.info?.id;
+ if (typeof messageId === 'string') {
+ if (seenIdsFromTail.has(messageId)) {
+ continue;
+ }
+ seenIdsFromTail.add(messageId);
+ }
+ dedupedMessages.push(message);
+ }
+ dedupedMessages.reverse();
+
+ const normalizedMessages = dedupedMessages
+ .map((message) => {
+ const filteredParts = filterSyntheticParts(message.parts);
+ const normalized = filteredParts === message.parts
+ ? message
+ : {
+ ...message,
+ parts: filteredParts,
+ };
+ return normalized;
+ });
+
+ const output: ChatMessageEntry[] = [];
+
+ for (let index = 0; index < normalizedMessages.length; index += 1) {
+ const current = normalizedMessages[index];
+ const previous = output.length > 0 ? output[output.length - 1] : undefined;
+
+ if (isUserSubtaskMessage(previous)) {
+ const bridge = isSyntheticSubtaskBridgeAssistant(current);
+ if (bridge.hide) {
+ output[output.length - 1] = withSubtaskSessionId(previous as ChatMessageEntry, bridge.taskSessionId);
+ continue;
+ }
+ }
+
+ if (isUserShellMarkerMessage(previous)) {
+ const bridge = getShellBridgeAssistantDetails(current, getMessageId(previous));
+ if (bridge.hide) {
+ output[output.length - 1] = withShellBridgeDetails(previous as ChatMessageEntry, bridge.details);
+ continue;
+ }
+ }
+
+ output.push(current);
+ }
+
+ return output;
+ }, [messages]);
+
+ const activeRetryStatus = useSessionStore(
+ useShallow((state) => {
+ const sessionId = state.currentSessionId;
+ if (!sessionId) return null;
+ const status = state.sessionStatus?.get(sessionId);
+ if (!status || status.type !== 'retry') return null;
+ const rawMessage = typeof status.message === 'string' ? status.message.trim() : '';
+ return {
+ sessionId,
+ message: rawMessage || 'Quota limit reached. Retrying automatically.',
+ confirmedAt: status.confirmedAt,
+ };
+ })
+ );
+
+ const activeRetrySessionId = activeRetryStatus?.sessionId ?? null;
+ const activeRetryMessage = activeRetryStatus?.message
+ ?? 'Quota limit reached. Retrying automatically.';
+ const activeRetryConfirmedAt = activeRetryStatus?.confirmedAt;
+
+ const [fallbackRetryTimestamp, setFallbackRetryTimestamp] = React.useState(0);
+ const fallbackRetrySessionRef = React.useRef(null);
+ const [scrollContainer, setScrollContainer] = React.useState(null);
+
+ React.useLayoutEffect(() => {
+ setScrollContainer(scrollRef?.current ?? null);
+ }, [scrollRef]);
+
+ React.useEffect(() => {
+ if (!activeRetryStatus || typeof activeRetryStatus.confirmedAt === 'number') {
+ fallbackRetrySessionRef.current = null;
+ setFallbackRetryTimestamp(0);
+ return;
+ }
+
+ if (fallbackRetrySessionRef.current !== activeRetryStatus.sessionId) {
+ fallbackRetrySessionRef.current = activeRetryStatus.sessionId;
+ setFallbackRetryTimestamp(Date.now());
+ }
+ }, [activeRetryStatus, activeRetryStatus?.sessionId, activeRetryStatus?.confirmedAt]);
+
+ const displayMessages = React.useMemo(() => {
+ if (!activeRetrySessionId) {
+ return baseDisplayMessages;
+ }
+
+ const retryError = {
+ name: 'SessionRetry',
+ message: activeRetryMessage,
+ data: { message: activeRetryMessage },
+ };
+
+ let lastUserIndex = -1;
+ for (let index = baseDisplayMessages.length - 1; index >= 0; index -= 1) {
+ if (resolveMessageRole(baseDisplayMessages[index]) === 'user') {
+ lastUserIndex = index;
+ break;
+ }
+ }
+
+ if (lastUserIndex < 0) {
+ return baseDisplayMessages;
+ }
+
+ // Prefer attaching retry error to the assistant message in the current turn (if one exists)
+ // to avoid rendering a separate header-only placeholder + error block.
+ let targetAssistantIndex = -1;
+ for (let index = baseDisplayMessages.length - 1; index > lastUserIndex; index -= 1) {
+ if (resolveMessageRole(baseDisplayMessages[index]) === 'assistant') {
+ targetAssistantIndex = index;
+ break;
+ }
+ }
+
+ if (targetAssistantIndex >= 0) {
+ const existing = baseDisplayMessages[targetAssistantIndex];
+ const existingInfo = existing.info as unknown as { error?: unknown };
+ if (existingInfo.error) {
+ return baseDisplayMessages;
+ }
+
+ return baseDisplayMessages.map((message, index) => {
+ if (index !== targetAssistantIndex) {
+ return message;
+ }
+ return {
+ ...message,
+ info: {
+ ...(message.info as unknown as Record),
+ error: retryError,
+ } as unknown as Message,
+ };
+ });
+ }
+
+ const eventTime = typeof activeRetryConfirmedAt === 'number' ? activeRetryConfirmedAt : fallbackRetryTimestamp;
+ const syntheticId = `synthetic_retry_notice_${activeRetrySessionId}`;
+ const synthetic: ChatMessageEntry = {
+ info: {
+ id: syntheticId,
+ sessionID: activeRetrySessionId,
+ role: 'assistant',
+ time: { created: eventTime, completed: eventTime },
+ finish: 'stop',
+ error: retryError,
+ } as unknown as Message,
+ parts: [],
+ };
+
+ const next = baseDisplayMessages.slice();
+ next.splice(lastUserIndex + 1, 0, synthetic);
+ return next;
+ }, [activeRetryMessage, activeRetryConfirmedAt, activeRetrySessionId, baseDisplayMessages, fallbackRetryTimestamp]);
+
+ const turns = React.useMemo(() => detectTurns(displayMessages), [displayMessages]);
+
+ const renderEntries = React.useMemo(() => {
+ const entries: RenderEntry[] = [];
+ const turnByUserId = new Map();
+ const groupedAssistantIds = new Set();
+ const lastTurn = turns.length > 0 ? turns[turns.length - 1] : null;
+ const lastTurnId = lastTurn?.turnId ?? null;
+ const lastTurnMessageIds = new Set();
+ if (lastTurn) {
+ lastTurnMessageIds.add(lastTurn.userMessage.info.id);
+ lastTurn.assistantMessages.forEach((assistantMessage: ChatMessageEntry) => {
+ lastTurnMessageIds.add(assistantMessage.info.id);
+ });
+ }
+
+ turns.forEach((turn: Turn) => {
+ turnByUserId.set(turn.userMessage.info.id, turn);
+ turn.assistantMessages.forEach((assistantMessage: ChatMessageEntry) => {
+ groupedAssistantIds.add(assistantMessage.info.id);
+ });
+ });
+
+ displayMessages.forEach((message: ChatMessageEntry) => {
+ const turn = turnByUserId.get(message.info.id);
+ if (turn) {
+ entries.push({
+ kind: 'turn',
+ key: `turn:${turn.turnId}`,
+ turn,
+ isLastTurn: turn.turnId === lastTurnId,
+ });
+ return;
+ }
+
+ if (groupedAssistantIds.has(message.info.id)) {
+ return;
+ }
+
+ entries.push({
+ kind: 'ungrouped',
+ key: `msg:${message.info.id}`,
+ message,
+ isInLastTurn: lastTurnMessageIds.has(message.info.id),
+ });
+ });
+
+ return entries;
+ }, [displayMessages, turns]);
+
+ const shouldVirtualize = Boolean(scrollContainer) && renderEntries.length >= MESSAGE_VIRTUALIZE_THRESHOLD;
+
+ const estimateEntrySize = React.useCallback(
+ (index: number): number => {
+ const entry = renderEntries[index];
+ if (!entry) {
+ return 300;
+ }
+ if (entry.kind === 'turn') {
+ const assistantCount = entry.turn.assistantMessages.length;
+ return Math.min(3600, 140 + assistantCount * 260);
+ }
+ const role = resolveMessageRole(entry.message);
+ return role === 'user' ? 120 : 280;
+ },
+ [renderEntries]
+ );
+
+ const virtualizer = useMessageListVirtualizer({
+ count: renderEntries.length,
+ getScrollElement: () => scrollContainer,
+ estimateSize: estimateEntrySize,
+ overscan: isMobile ? MESSAGE_VIRTUAL_OVERSCAN_MOBILE : MESSAGE_VIRTUAL_OVERSCAN_DESKTOP,
+ getItemKey: (index: number) => renderEntries[index]?.key ?? index,
+ enabled: shouldVirtualize,
+ useFlushSync: false,
+ });
+
+ const virtualRows = shouldVirtualize ? virtualizer.getVirtualItems() : [];
+
+ const scrollVirtualizerToIndex = React.useCallback((index: number, behavior: ScrollBehavior = 'auto') => {
+ if (!virtualizer) {
+ return;
+ }
+ const normalizedBehavior: 'auto' | 'smooth' = behavior === 'instant' ? 'auto' : behavior;
+ virtualizer.scrollToIndex(index, { align: 'start', behavior: normalizedBehavior });
+ }, [virtualizer]);
+
+ const messageIndexMap = React.useMemo(() => {
+ const indexMap = new Map();
+
+ renderEntries.forEach((entry, index) => {
+ if (entry.kind === 'ungrouped') {
+ indexMap.set(entry.message.info.id, index);
+ return;
+ }
+ indexMap.set(entry.turn.userMessage.info.id, index);
+ entry.turn.assistantMessages.forEach((message) => {
+ indexMap.set(message.info.id, index);
+ });
+ });
+
+ return indexMap;
+ }, [renderEntries]);
+
+ const findMessageElement = React.useCallback((messageId: string): HTMLElement | null => {
+ const container = scrollContainer;
+ if (!container) {
+ return null;
+ }
+ return container.querySelector(`[data-message-id="${messageId}"]`);
+ }, [scrollContainer]);
+
+ const scrollMessageElementIntoView = React.useCallback((messageId: string, behavior: ScrollBehavior = 'auto') => {
+ const container = scrollContainer;
+ if (!container) {
+ return false;
+ }
+ const messageElement = findMessageElement(messageId);
+ if (!messageElement) {
+ return false;
+ }
+
+ const containerRect = container.getBoundingClientRect();
+ const messageRect = messageElement.getBoundingClientRect();
+ const offset = 50;
+ const top = messageRect.top - containerRect.top + container.scrollTop - offset;
+ container.scrollTo({ top, behavior });
+ return true;
+ }, [findMessageElement, scrollContainer]);
+
+ React.useLayoutEffect(() => {
+ if (!ref) {
+ return;
+ }
+
+ const handle: MessageListHandle = {
+ scrollToMessageId: (messageId: string, options?: { behavior?: ScrollBehavior }) => {
+ const behavior = options?.behavior ?? 'auto';
+ const index = messageIndexMap.get(messageId);
+ if (index === undefined) {
+ return false;
+ }
+
+ if (shouldVirtualize) {
+ scrollVirtualizerToIndex(index, behavior === 'instant' ? 'auto' : behavior);
+ if (typeof window !== 'undefined') {
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ scrollMessageElementIntoView(messageId, behavior);
+ });
+ });
+ }
+ return true;
+ }
+
+ return scrollMessageElementIntoView(messageId, behavior);
+ },
+
+ captureViewportAnchor: () => {
+ const container = scrollContainer;
+ if (!container) {
+ return null;
+ }
+
+ const containerRect = container.getBoundingClientRect();
+ const nodes = Array.from(container.querySelectorAll('[data-message-id]'));
+ const firstVisible = nodes.find((node) => node.getBoundingClientRect().bottom > containerRect.top + 1);
+ if (!firstVisible) {
+ return null;
+ }
+
+ const messageId = firstVisible.dataset.messageId;
+ if (!messageId) {
+ return null;
+ }
+
+ return {
+ messageId,
+ offsetTop: firstVisible.getBoundingClientRect().top - containerRect.top,
+ };
+ },
+
+ restoreViewportAnchor: (anchor: { messageId: string; offsetTop: number }) => {
+ const container = scrollContainer;
+ if (!container) {
+ return false;
+ }
+
+ const index = messageIndexMap.get(anchor.messageId);
+ if (index === undefined) {
+ return false;
+ }
+
+ if (shouldVirtualize) {
+ scrollVirtualizerToIndex(index, 'auto');
+ }
+
+ if (typeof window !== 'undefined') {
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ const element = findMessageElement(anchor.messageId);
+ if (!element) {
+ return;
+ }
+ const containerRect = container.getBoundingClientRect();
+ const targetTop = element.getBoundingClientRect().top - containerRect.top;
+ const delta = targetTop - anchor.offsetTop;
+ if (delta !== 0) {
+ container.scrollTop += delta;
+ }
+ });
+ });
+ }
+
+ return true;
+ },
+ };
+
+ if (typeof ref === 'function') {
+ ref(handle);
+ return () => {
+ ref(null);
+ };
+ }
+
+ const objectRef = ref;
+ objectRef.current = handle;
+ return () => {
+ objectRef.current = null;
+ };
+ }, [findMessageElement, messageIndexMap, scrollMessageElementIntoView, scrollContainer, scrollVirtualizerToIndex, shouldVirtualize, ref]);
+
+ const disableFadeIn = shouldVirtualize && virtualizer.isScrolling;
+
+ return (
+
+
+ {hasRenderEarlier && (
+
+
+ Render earlier messages
+
+
+ )}
+
+ {hasMoreAbove && (
+
+ {isLoadingOlder ? (
+
+ Loading…
+
+ ) : (
+
+ Load older messages
+
+ )}
+
+ )}
+
+
+ {shouldVirtualize ? (
+
+ {virtualRows.map((virtualRow: VirtualItem) => {
+ const entry = renderEntries[virtualRow.index];
+ if (!entry) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ })}
+
+ ) : (
+
+ )}
+
+
+ {(questions.length > 0 || permissions.length > 0) && (
+
+ {questions.map((question) => (
+
+ ))}
+ {permissions.map((permission) => (
+
+ ))}
+
+ )}
+
+ {/* Bottom spacer */}
+
+
+
+ );
+});
+
+MessageList.displayName = 'MessageList';
+
+export default React.memo(MessageList);
diff --git a/ui/src/components/chat/MobileAgentButton.tsx b/ui/src/components/chat/MobileAgentButton.tsx
new file mode 100644
index 0000000..944a6f3
--- /dev/null
+++ b/ui/src/components/chat/MobileAgentButton.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { getAgentDisplayName } from './mobileControlsUtils';
+import { getAgentColor } from '@/lib/agentColors';
+
+interface MobileAgentButtonProps {
+ onCycleAgent: () => void;
+ onOpenAgentPanel: () => void;
+ className?: string;
+}
+
+const LONG_PRESS_MS = 500;
+
+// NOTE: Use pointer events instead of onClick to keep soft keyboard open on mobile
+export const MobileAgentButton: React.FC = ({ onCycleAgent, onOpenAgentPanel, className }) => {
+ const { currentAgentName, getVisibleAgents } = useConfigStore();
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+ const sessionAgentName = useSessionStore((state) =>
+ currentSessionId ? state.getSessionAgentSelection(currentSessionId) : null
+ );
+
+ const agents = getVisibleAgents();
+ const uiAgentName = currentSessionId ? (sessionAgentName || currentAgentName) : currentAgentName;
+ const agentLabel = getAgentDisplayName(agents, uiAgentName);
+ const agentColor = getAgentColor(uiAgentName);
+
+ const longPressTimerRef = React.useRef | null>(null);
+ const isLongPressRef = React.useRef(false);
+
+ const handlePointerDown = () => {
+ isLongPressRef.current = false;
+ longPressTimerRef.current = setTimeout(() => {
+ isLongPressRef.current = true;
+ onOpenAgentPanel();
+ }, LONG_PRESS_MS);
+ };
+
+ // Use onPointerUp (not onClick) to prevent focus transfer that closes mobile keyboard
+ const handlePointerUp = () => {
+ if (longPressTimerRef.current) {
+ clearTimeout(longPressTimerRef.current);
+ longPressTimerRef.current = null;
+ }
+ if (!isLongPressRef.current) {
+ onCycleAgent();
+ }
+ };
+
+ const handlePointerLeave = () => {
+ if (longPressTimerRef.current) {
+ clearTimeout(longPressTimerRef.current);
+ longPressTimerRef.current = null;
+ }
+ };
+
+ React.useEffect(() => {
+ return () => {
+ if (longPressTimerRef.current) {
+ clearTimeout(longPressTimerRef.current);
+ }
+ };
+ }, []);
+
+ return (
+ e.preventDefault()}
+ className={cn(
+ 'inline-flex min-w-0 items-center select-none',
+ 'rounded-lg border border-border/50 px-1.5',
+ 'typography-micro font-medium',
+ 'focus:outline-none hover:bg-[var(--interactive-hover)]',
+ 'touch-none',
+ className
+ )}
+ style={{
+ height: '26px',
+ maxHeight: '26px',
+ minHeight: '26px',
+ color: `var(${agentColor.var})`,
+ }}
+ title={agentLabel}
+ >
+ {agentLabel}
+
+ );
+};
+
+export default MobileAgentButton;
diff --git a/ui/src/components/chat/MobileModelButton.tsx b/ui/src/components/chat/MobileModelButton.tsx
new file mode 100644
index 0000000..04b316e
--- /dev/null
+++ b/ui/src/components/chat/MobileModelButton.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { getModelDisplayName } from './mobileControlsUtils';
+
+interface MobileModelButtonProps {
+ onOpenModel: () => void;
+ className?: string;
+}
+
+export const MobileModelButton: React.FC = ({ onOpenModel, className }) => {
+ const { currentModelId, getCurrentProvider } = useConfigStore();
+ const currentProvider = getCurrentProvider();
+ const modelLabel = getModelDisplayName(currentProvider, currentModelId);
+
+ return (
+
+
+ {modelLabel}
+
+
+ );
+};
+
+export default MobileModelButton;
diff --git a/ui/src/components/chat/MobileSessionStatusBar.tsx b/ui/src/components/chat/MobileSessionStatusBar.tsx
new file mode 100644
index 0000000..b3b3360
--- /dev/null
+++ b/ui/src/components/chat/MobileSessionStatusBar.tsx
@@ -0,0 +1,1598 @@
+import React from 'react';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { useProjectsStore } from '@/stores/useProjectsStore';
+import type { Session } from '@opencode-ai/sdk/v2';
+import type { ProjectEntry } from '@/lib/api/types';
+import { cn, formatDirectoryName } from '@/lib/utils';
+import { getAgentColor } from '@/lib/agentColors';
+import {
+ RiLoader4Line,
+ RiAddLine,
+ RiDragMove2Line,
+ RiDeleteBinLine,
+ RiEditLine,
+ RiArrowUpLine,
+ RiArrowDownLine,
+} from '@remixicon/react';
+import {
+ DndContext,
+ closestCenter,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ type DragEndEvent,
+} from '@dnd-kit/core';
+import {
+ arrayMove,
+ SortableContext,
+ sortableKeyboardCoordinates,
+ useSortable,
+ verticalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
+import type { SessionContextUsage } from '@/stores/types/sessionTypes';
+import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
+import { useDirectoryStore } from '@/stores/useDirectoryStore';
+import { toast } from '@/components/ui';
+import { isTauriShell, isDesktopLocalOriginActive, requestDirectoryAccess } from '@/lib/desktop';
+import { sessionEvents } from '@/lib/sessionEvents';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
+import { useDrawerSwipe } from '@/hooks/useDrawerSwipe';
+import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
+import { useThemeSystem } from '@/contexts/useThemeSystem';
+
+interface MobileSessionStatusBarProps {
+ onSessionSwitch?: (sessionId: string) => void;
+ cornerRadius?: number;
+}
+
+interface SessionWithStatus extends Session {
+ _statusType?: 'busy' | 'retry' | 'idle';
+ _hasRunningChildren?: boolean;
+ _runningChildrenCount?: number;
+ _childIndicators?: Array<{ session: Session; isRunning: boolean }>;
+}
+
+// Normalize path for comparison
+const normalize = (value: string): string => {
+ if (!value) return '';
+ const replaced = value.replace(/\\/g, '/');
+ return replaced === '/' ? '/' : replaced.replace(/\/+$/, '');
+};
+
+function useSessionGrouping(
+ sessions: Session[],
+ sessionStatus: Map | undefined,
+ sessionAttentionStates: Map | undefined
+) {
+ const parentChildMap = React.useMemo(() => {
+ const map = new Map();
+ const allIds = new Set(sessions.map((s) => s.id));
+
+ sessions.forEach((session) => {
+ const parentID = (session as { parentID?: string }).parentID;
+ if (parentID && allIds.has(parentID)) {
+ map.set(parentID, [...(map.get(parentID) || []), session]);
+ }
+ });
+ return map;
+ }, [sessions]);
+
+ const getStatusType = React.useCallback((sessionId: string): 'busy' | 'retry' | 'idle' => {
+ const status = sessionStatus?.get(sessionId);
+ if (status?.type === 'busy' || status?.type === 'retry') return status.type;
+ return 'idle';
+ }, [sessionStatus]);
+
+ const hasRunningChildren = React.useCallback((sessionId: string): boolean => {
+ const children = parentChildMap.get(sessionId) || [];
+ return children.some((child) => getStatusType(child.id) !== 'idle');
+ }, [parentChildMap, getStatusType]);
+
+ const getRunningChildrenCount = React.useCallback((sessionId: string): number => {
+ const children = parentChildMap.get(sessionId) || [];
+ return children.filter((child) => getStatusType(child.id) !== 'idle').length;
+ }, [parentChildMap, getStatusType]);
+
+ const getChildIndicators = React.useCallback((sessionId: string): Array<{ session: Session; isRunning: boolean }> => {
+ const children = parentChildMap.get(sessionId) || [];
+ return children
+ .filter((child) => getStatusType(child.id) !== 'idle')
+ .map((child) => ({ session: child, isRunning: true }))
+ .slice(0, 3);
+ }, [parentChildMap, getStatusType]);
+
+ const processedSessions = React.useMemo(() => {
+ const topLevel = sessions.filter((session) => {
+ const parentID = (session as { parentID?: string }).parentID;
+ return !parentID || !new Set(sessions.map((s) => s.id)).has(parentID);
+ });
+
+ const running: SessionWithStatus[] = [];
+ const viewed: SessionWithStatus[] = [];
+
+ topLevel.forEach((session) => {
+ const statusType = getStatusType(session.id);
+ const hasRunning = hasRunningChildren(session.id);
+ const attention = sessionAttentionStates?.get(session.id)?.needsAttention ?? false;
+
+ const enriched: SessionWithStatus = {
+ ...session,
+ _statusType: statusType,
+ _hasRunningChildren: hasRunning,
+ _runningChildrenCount: getRunningChildrenCount(session.id),
+ _childIndicators: getChildIndicators(session.id),
+ };
+
+ if (statusType !== 'idle' || hasRunning) {
+ running.push(enriched);
+ } else if (attention) {
+ running.push(enriched);
+ } else {
+ viewed.push(enriched);
+ }
+ });
+
+ const sortByUpdated = (a: Session, b: Session) => {
+ const aTime = (a as unknown as { time?: { updated?: number } }).time?.updated ?? 0;
+ const bTime = (b as unknown as { time?: { updated?: number } }).time?.updated ?? 0;
+ return bTime - aTime;
+ };
+
+ running.sort(sortByUpdated);
+ viewed.sort(sortByUpdated);
+
+ return [...running, ...viewed];
+ }, [sessions, getStatusType, hasRunningChildren, getRunningChildrenCount, getChildIndicators, sessionAttentionStates]);
+
+ const totalRunning = processedSessions.reduce((sum, s) => {
+ const selfRunning = s._statusType !== 'idle' ? 1 : 0;
+ return sum + selfRunning + (s._runningChildrenCount ?? 0);
+ }, 0);
+
+ const totalUnread = processedSessions.filter((s) => sessionAttentionStates?.get(s.id)?.needsAttention ?? false).length;
+
+ return { sessions: processedSessions, totalRunning, totalUnread, totalCount: processedSessions.length };
+}
+
+function useSessionHelpers(
+ agents: Array<{ name: string }>,
+ sessionStatus: Map | undefined,
+ sessionAttentionStates: Map | undefined
+) {
+ const getSessionAgentName = React.useCallback((session: Session): string => {
+ const agent = (session as { agent?: string }).agent;
+ if (agent) return agent;
+
+ const sessionAgentSelection = useSessionStore.getState().getSessionAgentSelection(session.id);
+ if (sessionAgentSelection) return sessionAgentSelection;
+
+ return agents[0]?.name ?? 'agent';
+ }, [agents]);
+
+ const getSessionTitle = React.useCallback((session: Session): string => {
+ const title = session.title;
+ if (title && title.trim()) return title;
+ return 'New session';
+ }, []);
+
+ const isRunning = React.useCallback((sessionId: string): boolean => {
+ const status = sessionStatus?.get(sessionId);
+ return status?.type === 'busy' || status?.type === 'retry';
+ }, [sessionStatus]);
+
+ // Use server-authoritative attention state instead of local activity state
+ const needsAttention = React.useCallback((sessionId: string): boolean => {
+ return sessionAttentionStates?.get(sessionId)?.needsAttention ?? false;
+ }, [sessionAttentionStates]);
+
+ return { getSessionAgentName, getSessionTitle, isRunning, needsAttention };
+}
+
+// Hook to calculate project status indicators
+function useProjectStatus(
+ sessions: Session[],
+ sessionStatus: Map | undefined,
+ sessionAttentionStates: Map | undefined,
+ currentSessionId: string | null
+) {
+ const availableWorktreesByProject = useSessionStore((state) => state.availableWorktreesByProject);
+ const sessionsByDirectory = useSessionStore((state) => state.sessionsByDirectory);
+ const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
+
+ const projectStatusMap = React.useCallback((projectPath: string): { hasRunning: boolean; hasUnread: boolean } => {
+ const getStatusType = (sessionId: string): 'busy' | 'retry' | 'idle' => {
+ const status = sessionStatus?.get(sessionId);
+ if (status?.type === 'busy' || status?.type === 'retry') return status.type;
+ return 'idle';
+ };
+
+ const projectRoot = normalize(projectPath);
+ if (!projectRoot) {
+ return { hasRunning: false, hasUnread: false };
+ }
+
+ const dirs: string[] = [projectRoot];
+ const worktrees = availableWorktreesByProject.get(projectRoot) ?? [];
+ for (const meta of worktrees) {
+ const p = (meta && typeof meta === 'object' && 'path' in meta) ? (meta as { path?: unknown }).path : null;
+ if (typeof p === 'string' && p.trim()) {
+ const normalized = normalize(p);
+ if (normalized && normalized !== projectRoot) {
+ dirs.push(normalized);
+ }
+ }
+ }
+
+ const seen = new Set();
+ let hasRunning = false;
+ let hasUnread = false;
+
+ for (const dir of dirs) {
+ const list = sessionsByDirectory.get(dir) ?? getSessionsByDirectory(dir);
+ for (const session of list) {
+ if (!session?.id || seen.has(session.id)) {
+ continue;
+ }
+ seen.add(session.id);
+
+ const statusType = getStatusType(session.id);
+ if (statusType === 'busy' || statusType === 'retry') {
+ hasRunning = true;
+ }
+
+ if (session.id !== currentSessionId && sessionAttentionStates?.get(session.id)?.needsAttention === true) {
+ hasUnread = true;
+ }
+
+ if (hasRunning && hasUnread) {
+ break;
+ }
+ }
+ if (hasRunning && hasUnread) {
+ break;
+ }
+ }
+
+ return { hasRunning, hasUnread };
+ }, [sessionsByDirectory, getSessionsByDirectory, availableWorktreesByProject, sessionStatus, sessionAttentionStates, currentSessionId]);
+
+ return projectStatusMap;
+}
+
+function StatusIndicator({ isRunning, needsAttention }: { isRunning: boolean; needsAttention: boolean }) {
+ if (isRunning) {
+ return ;
+ }
+ if (needsAttention) {
+ return
;
+ }
+ return
;
+}
+
+function RunningIndicator({ count }: { count: number }) {
+ if (count === 0) return null;
+ return (
+
+
+ {count}
+
+ );
+}
+
+function UnreadIndicator({ count }: { count: number }) {
+ if (count === 0) return null;
+ return (
+
+
+ {count}
+
+ );
+}
+
+function SessionItem({
+ session,
+ isCurrent,
+ getSessionAgentName,
+ getSessionTitle,
+ onClick,
+ onDoubleClick,
+ needsAttention
+}: {
+ session: SessionWithStatus;
+ isCurrent: boolean;
+ getSessionAgentName: (s: Session) => string;
+ getSessionTitle: (s: Session) => string;
+ onClick: () => void;
+ onDoubleClick?: () => void;
+ needsAttention: (sessionId: string) => boolean;
+}) {
+ const agentName = getSessionAgentName(session);
+ const agentColor = getAgentColor(agentName);
+ const extraCount = (session._runningChildrenCount || 0) + (session._statusType !== 'idle' ? 1 : 0) - 1 - (session._childIndicators?.length || 0);
+
+ return (
+ {
+ e.stopPropagation();
+ onDoubleClick?.();
+ }}
+ className={cn(
+ "flex items-center gap-0.5 px-1.5 py-px text-left transition-colors",
+ "hover:bg-[var(--interactive-hover)] active:bg-[var(--interactive-selection)]",
+ isCurrent && "bg-[var(--interactive-selection)]/30"
+ )}
+ >
+
+
+
+
+
+
+
+ {getSessionTitle(session)}
+
+
+ {(session._childIndicators?.length || 0) > 0 && (
+
+
[
+
+ {session._childIndicators!.map(({ session: child }) => {
+ const childColor = getAgentColor(getSessionAgentName(child));
+ return (
+
+
+
+ );
+ })}
+ {extraCount > 0 && (
+
+ +{extraCount}
+
+ )}
+
+
]
+
+ )}
+
+ );
+}
+
+function TokenUsageIndicator({ contextUsage }: { contextUsage: SessionContextUsage | null }) {
+ if (!contextUsage || contextUsage.totalTokens === 0) return null;
+
+ const percentage = Math.min(contextUsage.percentage, 999);
+ const colorClass =
+ percentage >= 90 ? 'text-[var(--status-error)]' :
+ percentage >= 75 ? 'text-[var(--status-warning)]' : 'text-[var(--status-success)]';
+
+ return (
+
+ {percentage.toFixed(1)}%
+
+ );
+}
+
+interface SessionStatusHeaderProps {
+ currentSessionTitle: string;
+ currentProjectLabel?: string;
+ currentProjectIcon?: string | null;
+ currentProjectIconImageUrl?: string | null;
+ currentProjectIconBackground?: string | null;
+ currentProjectColor?: string | null;
+ onToggle: () => void;
+ isExpanded?: boolean;
+ childIndicators?: Array<{ session: Session; isRunning: boolean }>;
+}
+
+function SessionStatusHeader({
+ currentSessionTitle,
+ currentProjectLabel,
+ currentProjectIcon,
+ currentProjectIconImageUrl,
+ currentProjectIconBackground,
+ currentProjectColor,
+ onToggle,
+ isExpanded = false,
+ childIndicators = []
+}: SessionStatusHeaderProps) {
+ const [imageFailed, setImageFailed] = React.useState(false);
+ const ProjectIcon = currentProjectIcon ? PROJECT_ICON_MAP[currentProjectIcon] : null;
+ const imageUrl = !imageFailed ? currentProjectIconImageUrl : null;
+ const projectColorVar = currentProjectColor ? (PROJECT_COLOR_MAP[currentProjectColor] ?? null) : null;
+ const extraCount = childIndicators.length > 3 ? childIndicators.length - 3 : 0;
+
+ React.useEffect(() => {
+ setImageFailed(false);
+ }, [currentProjectIconImageUrl]);
+
+ return (
+
+ {!isExpanded && currentProjectLabel && (
+
+
+ {imageUrl ? (
+
+ setImageFailed(true)}
+ />
+
+ ) : ProjectIcon && (
+
+ )}
+
+ {currentProjectLabel}
+
+
+
+
+ )}
+
+
+ {currentSessionTitle}
+
+ {childIndicators.length > 0 && (
+
+
[
+
+ {childIndicators.slice(0, 3).map((child) => {
+ const childAgent = (child.session as { agent?: string }).agent || 'agent';
+ const childColor = getAgentColor(childAgent);
+ return (
+
+
+
+ );
+ })}
+ {extraCount > 0 && (
+
+ +{extraCount}
+
+ )}
+
+
]
+
+ )}
+
+
+ );
+}
+
+
+
+// Hook for long press with movement detection
+function useLongPress(
+ onLongPress: () => void,
+ onClick: () => void,
+ ms = 500
+) {
+ const timerRef = React.useRef(null);
+ const isLongPress = React.useRef(false);
+ const startPosRef = React.useRef<{ x: number; y: number } | null>(null);
+ const hasMovedRef = React.useRef(false);
+ const MOVE_THRESHOLD = 10; // pixels
+
+ const start = React.useCallback((clientX: number, clientY: number) => {
+ isLongPress.current = false;
+ hasMovedRef.current = false;
+ startPosRef.current = { x: clientX, y: clientY };
+ timerRef.current = setTimeout(() => {
+ if (!hasMovedRef.current) {
+ isLongPress.current = true;
+ onLongPress();
+ }
+ }, ms);
+ }, [onLongPress, ms]);
+
+ const move = React.useCallback((clientX: number, clientY: number) => {
+ if (!startPosRef.current) return;
+
+ const dx = Math.abs(clientX - startPosRef.current.x);
+ const dy = Math.abs(clientY - startPosRef.current.y);
+
+ if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) {
+ hasMovedRef.current = true;
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ }
+ }, []);
+
+ const end = React.useCallback(() => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ startPosRef.current = null;
+ }, []);
+
+ const handleClick = React.useCallback(() => {
+ if (!isLongPress.current) {
+ onClick();
+ }
+ }, [onClick]);
+
+ return {
+ onMouseDown: (e: React.MouseEvent) => start(e.clientX, e.clientY),
+ onMouseUp: end,
+ onMouseLeave: end,
+ onMouseMove: (e: React.MouseEvent) => move(e.clientX, e.clientY),
+ onTouchStart: (e: React.TouchEvent) => {
+ const touch = e.touches[0];
+ start(touch.clientX, touch.clientY);
+ },
+ onTouchMove: (e: React.TouchEvent) => {
+ const touch = e.touches[0];
+ move(touch.clientX, touch.clientY);
+ },
+ onTouchEnd: end,
+ onClick: handleClick,
+ };
+}
+
+// Sortable project item for edit panel
+interface SortableProjectItemProps {
+ project: ProjectEntry;
+ isFirst: boolean;
+ isLast: boolean;
+ onMoveUp: () => void;
+ onMoveDown: () => void;
+ onEdit: () => void;
+ onDelete: () => void;
+ formatProjectLabel: (project: ProjectEntry) => string;
+}
+
+function SortableProjectItem({
+ project,
+ isFirst,
+ isLast,
+ onMoveUp,
+ onMoveDown,
+ onEdit,
+ onDelete,
+ formatProjectLabel,
+}: SortableProjectItemProps) {
+ const { currentTheme } = useThemeSystem();
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id: project.id });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ zIndex: isDragging ? 10 : 1,
+ };
+
+ const [imageFailed, setImageFailed] = React.useState(false);
+ const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
+ const projectIconImageUrl = !imageFailed
+ ? getProjectIconImageUrl(project, {
+ themeVariant: currentTheme.metadata.variant,
+ iconColor: currentTheme.colors.surface.foreground,
+ })
+ : null;
+ const projectColorVar = project.color ? (PROJECT_COLOR_MAP[project.color] ?? null) : null;
+
+ return (
+
+ {/* Drag handle */}
+
+
+
+
+ {/* Project info */}
+
+ {projectIconImageUrl ? (
+
+ setImageFailed(true)}
+ />
+
+ ) : ProjectIcon ? (
+
+ ) : (
+
+ )}
+
+ {formatProjectLabel(project)}
+
+
+
+ {/* Actions */}
+
+ {/* Move up/down buttons (for non-drag sorting) */}
+
+
+
+
+
+
+
+
+
+ {/* Edit button */}
+
+
+
+
+ {/* Delete button */}
+
+
+
+
+
+ );
+}
+
+// Project edit panel for mobile
+interface ProjectEditPanelProps {
+ isOpen: boolean;
+ onClose: () => void;
+ projects: ProjectEntry[];
+ onReorder: (fromIndex: number, toIndex: number) => void;
+ onEdit: (project: ProjectEntry) => void;
+ onDelete: (project: ProjectEntry) => void;
+ homeDirectory: string | null;
+}
+
+function ProjectEditPanel({
+ isOpen,
+ onClose,
+ projects,
+ onReorder,
+ onEdit,
+ onDelete,
+ homeDirectory,
+}: ProjectEditPanelProps) {
+ const [localProjects, setLocalProjects] = React.useState(projects);
+
+ React.useEffect(() => {
+ setLocalProjects(projects);
+ }, [projects, isOpen]);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ }),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ );
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event;
+ if (over && active.id !== over.id) {
+ const oldIndex = localProjects.findIndex((p) => p.id === active.id);
+ const newIndex = localProjects.findIndex((p) => p.id === over.id);
+
+ setLocalProjects((items) => arrayMove(items, oldIndex, newIndex));
+ onReorder(oldIndex, newIndex);
+ }
+ };
+
+ const handleMoveUp = (index: number) => {
+ if (index > 0) {
+ setLocalProjects((items) => arrayMove(items, index, index - 1));
+ onReorder(index, index - 1);
+ }
+ };
+
+ const handleMoveDown = (index: number) => {
+ if (index < localProjects.length - 1) {
+ setLocalProjects((items) => arrayMove(items, index, index + 1));
+ onReorder(index, index + 1);
+ }
+ };
+
+ const formatProjectLabel = (project: ProjectEntry): string => {
+ return project.label?.trim()
+ || formatDirectoryName(project.path, homeDirectory)
+ || project.path;
+ };
+
+ return (
+
+ Drag items to reorder, or use arrows to move. Tap edit to change details.
+
+ }
+ >
+
+
+ p.id)}
+ strategy={verticalListSortingStrategy}
+ >
+ {localProjects.map((project, index) => (
+ handleMoveUp(index)}
+ onMoveDown={() => handleMoveDown(index)}
+ onEdit={() => onEdit(project)}
+ onDelete={() => onDelete(project)}
+ formatProjectLabel={formatProjectLabel}
+ />
+ ))}
+
+
+
+ {localProjects.length === 0 && (
+
+ No projects to edit
+
+ )}
+
+
+ );
+}
+
+// Project button component with long press support
+interface ProjectButtonProps {
+ project: ProjectEntry;
+ isActive: boolean;
+ status: { hasRunning: boolean; hasUnread: boolean };
+ projectColorVar: string | null;
+ onProjectSwitch: () => void;
+ onOpenEditPanel?: () => void;
+ formatProjectLabel: (project: ProjectEntry) => string;
+}
+
+function ProjectButton({
+ project,
+ isActive,
+ status,
+ projectColorVar,
+ onProjectSwitch,
+ onOpenEditPanel,
+ formatProjectLabel,
+}: ProjectButtonProps) {
+ const { currentTheme } = useThemeSystem();
+ const [imageFailed, setImageFailed] = React.useState(false);
+ const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
+ const projectIconImageUrl = !imageFailed
+ ? getProjectIconImageUrl(project, {
+ themeVariant: currentTheme.metadata.variant,
+ iconColor: currentTheme.colors.surface.foreground,
+ })
+ : null;
+
+ React.useEffect(() => {
+ setImageFailed(false);
+ }, [project.id, project.iconImage?.updatedAt]);
+
+ const longPressHandlers = useLongPress(
+ () => {
+ if (onOpenEditPanel) {
+ onOpenEditPanel();
+ }
+ },
+ onProjectSwitch,
+ 600
+ );
+
+ return (
+
+ {/* Status indicators */}
+
+ {status.hasRunning && (
+
+ )}
+ {!status.hasRunning && status.hasUnread && (
+
+ )}
+
+
+ {/* Icon */}
+ {projectIconImageUrl ? (
+
+ setImageFailed(true)}
+ />
+
+ ) : ProjectIcon && (
+
+ )}
+
+ {/* Label */}
+
+ {formatProjectLabel(project)}
+
+
+ );
+}
+
+// Project bar component for expanded view
+interface ProjectBarProps {
+ projects: ProjectEntry[];
+ activeProjectId: string | null;
+ getProjectStatus: (path: string) => { hasRunning: boolean; hasUnread: boolean };
+ onProjectSwitch: (projectId: string) => void;
+ onAddProject: () => void;
+ onRemoveProject?: (projectId: string) => void;
+ homeDirectory: string | null;
+}
+
+function ProjectBar({
+ projects,
+ activeProjectId,
+ getProjectStatus,
+ onProjectSwitch,
+ onAddProject,
+ onRemoveProject,
+ homeDirectory
+}: ProjectBarProps) {
+ const scrollRef = React.useRef(null);
+ const [editPanelOpen, setEditPanelOpen] = React.useState(false);
+ const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
+ const [projectToDelete, setProjectToDelete] = React.useState(null);
+ const reorderProjects = useProjectsStore((state) => state.reorderProjects);
+
+ // Scroll active project into view
+ React.useEffect(() => {
+ if (scrollRef.current && activeProjectId) {
+ const activeElement = scrollRef.current.querySelector(`[data-project-id="${activeProjectId}"]`);
+ if (activeElement) {
+ activeElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
+ }
+ }
+ }, [activeProjectId]);
+
+ const handleOpenEditPanel = () => {
+ setEditPanelOpen(true);
+ };
+
+ const handleReorder = (fromIndex: number, toIndex: number) => {
+ reorderProjects(fromIndex, toIndex);
+ };
+
+ const [editingProject, setEditingProject] = React.useState(null);
+ const updateProjectMeta = useProjectsStore((state) => state.updateProjectMeta);
+
+ const handleEditProject = (project: ProjectEntry) => {
+ setEditingProject(project);
+ };
+
+ const handleSaveProjectEdit = (data: { label: string; icon: string | null; color: string | null; iconBackground: string | null }) => {
+ if (editingProject) {
+ updateProjectMeta(editingProject.id, data);
+ }
+ setEditingProject(null);
+ };
+
+ const handleDeleteProject = (project: ProjectEntry) => {
+ setProjectToDelete(project);
+ setDeleteDialogOpen(true);
+ };
+
+ const handleConfirmDelete = () => {
+ if (projectToDelete && onRemoveProject) {
+ onRemoveProject(projectToDelete.id);
+ }
+ setDeleteDialogOpen(false);
+ setProjectToDelete(null);
+ };
+
+ if (projects.length === 0) {
+ return (
+
+ No projects
+
+
+
+
+ );
+ }
+
+ const formatProjectLabel = (project: ProjectEntry): string => {
+ return project.label?.trim()
+ || formatDirectoryName(project.path, homeDirectory)
+ || project.path;
+ };
+
+ // Handle touch events to prevent drawer swipe when scrolling project bar
+ const handleTouchStart = (e: React.TouchEvent) => {
+ // Store initial touch position for this component
+ (e.currentTarget as HTMLElement).dataset.touchStartX = String(e.touches[0].clientX);
+ (e.currentTarget as HTMLElement).dataset.touchStartY = String(e.touches[0].clientY);
+ };
+
+ const handleTouchMove = (e: React.TouchEvent) => {
+ const target = e.currentTarget as HTMLElement;
+ const startX = Number(target.dataset.touchStartX || 0);
+ const startY = Number(target.dataset.touchStartY || 0);
+ const deltaX = e.touches[0].clientX - startX;
+ const deltaY = e.touches[0].clientY - startY;
+
+ // If horizontal scroll dominates, prevent default to stop drawer gesture
+ if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 5) {
+ e.stopPropagation();
+ }
+ };
+
+ const handleTouchEnd = (e: React.TouchEvent) => {
+ // Clean up
+ const target = e.currentTarget as HTMLElement;
+ delete target.dataset.touchStartX;
+ delete target.dataset.touchStartY;
+ };
+
+ return (
+
+
+ {projects.map((project) => {
+ const isActive = project.id === activeProjectId;
+ const status = getProjectStatus(project.path);
+ const projectColorVar = project.color ? (PROJECT_COLOR_MAP[project.color] ?? null) : null;
+
+ return (
+
onProjectSwitch(project.id)}
+ onOpenEditPanel={handleOpenEditPanel}
+ formatProjectLabel={formatProjectLabel}
+ />
+ );
+ })}
+
+
+ {/* Add project button */}
+
+
+
+
+ {/* Delete confirmation dialog */}
+
+
+
+ Remove Project
+
+ Are you sure you want to remove {projectToDelete?.label || formatDirectoryName(projectToDelete?.path || '', homeDirectory)} ?
+
+
+
+ setDeleteDialogOpen(false)}>
+ Cancel
+
+
+ Remove
+
+
+
+
+
+ {/* Project edit panel */}
+
setEditPanelOpen(false)}
+ projects={projects}
+ onReorder={handleReorder}
+ onEdit={handleEditProject}
+ onDelete={handleDeleteProject}
+ homeDirectory={homeDirectory}
+ />
+
+ {/* Project edit dialog */}
+ {editingProject && (
+ {
+ if (!open) setEditingProject(null);
+ }}
+ projectId={editingProject.id}
+ projectName={editingProject.label || formatDirectoryName(editingProject.path, homeDirectory)}
+ projectPath={editingProject.path}
+ initialIcon={editingProject.icon}
+ initialColor={editingProject.color}
+ initialIconBackground={editingProject.iconBackground}
+ onSave={handleSaveProjectEdit}
+ />
+ )}
+
+ );
+}
+
+function CollapsedView({
+ runningCount,
+ unreadCount,
+ currentSessionTitle,
+ currentProjectLabel,
+ currentProjectIcon,
+ currentProjectIconImageUrl,
+ currentProjectIconBackground,
+ currentProjectColor,
+ onToggle,
+ onNewSession,
+ cornerRadius,
+ contextUsage,
+ childIndicators = [],
+}: {
+ runningCount: number;
+ unreadCount: number;
+ currentSessionTitle: string;
+ currentProjectLabel?: string;
+ currentProjectIcon?: string | null;
+ currentProjectIconImageUrl?: string | null;
+ currentProjectIconBackground?: string | null;
+ currentProjectColor?: string | null;
+ onToggle: () => void;
+ onNewSession: () => void;
+ cornerRadius?: number;
+ contextUsage: SessionContextUsage | null;
+ childIndicators?: Array<{ session: Session; isRunning: boolean }>;
+}) {
+ const { handleTouchStart, handleTouchMove, handleTouchEnd } = useDrawerSwipe();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {
+ e.stopPropagation();
+ onNewSession();
+ }}
+ className="flex items-center gap-0.5 px-2 py-1 text-[12px] leading-tight !min-h-0 rounded border border-[var(--primary-base)]/60 bg-[var(--primary-base)]/5 text-[var(--primary-base)]/80 hover:text-[var(--primary-base)] hover:bg-[var(--primary-base)]/10 self-center"
+ >
+ New
+
+
+
+ );
+}
+
+function ExpandedView({
+ sessions,
+ currentSessionId,
+ runningCount,
+ unreadCount,
+ currentSessionTitle,
+ currentProjectLabel,
+ currentProjectIcon,
+ currentProjectIconImageUrl,
+ currentProjectIconBackground,
+ currentProjectColor,
+ isExpanded,
+ onToggleCollapse,
+ onNewSession,
+ onSessionClick,
+ onSessionDoubleClick,
+ onProjectSwitch,
+ onAddProject,
+ onRemoveProject,
+ getSessionAgentName,
+ getSessionTitle,
+ needsAttention,
+ cornerRadius,
+ contextUsage,
+ projects,
+ activeProjectId,
+ getProjectStatus,
+ homeDirectory,
+ childIndicators = [],
+}: {
+ sessions: SessionWithStatus[];
+ currentSessionId: string;
+ runningCount: number;
+ unreadCount: number;
+ currentSessionTitle: string;
+ currentProjectLabel?: string;
+ currentProjectIcon?: string | null;
+ currentProjectIconImageUrl?: string | null;
+ currentProjectIconBackground?: string | null;
+ currentProjectColor?: string | null;
+ isExpanded: boolean;
+ onToggleCollapse: () => void;
+ onNewSession: () => void;
+ onSessionClick: (id: string) => void;
+ onSessionDoubleClick?: () => void;
+ onProjectSwitch: (projectId: string) => void;
+ onAddProject: () => void;
+ onRemoveProject?: (projectId: string) => void;
+ getSessionAgentName: (s: Session) => string;
+ getSessionTitle: (s: Session) => string;
+ needsAttention: (sessionId: string) => boolean;
+ cornerRadius?: number;
+ contextUsage: SessionContextUsage | null;
+ projects: ProjectEntry[];
+ activeProjectId: string | null;
+ getProjectStatus: (path: string) => { hasRunning: boolean; hasUnread: boolean };
+ homeDirectory: string | null;
+ childIndicators?: Array<{ session: Session; isRunning: boolean }>;
+}) {
+ const containerRef = React.useRef(null);
+ const [collapsedHeight, setCollapsedHeight] = React.useState(null);
+ const [hasMeasured, setHasMeasured] = React.useState(false);
+ const { handleTouchStart, handleTouchMove, handleTouchEnd } = useDrawerSwipe();
+ const availableWorktreesByProject = useSessionStore((state) => state.availableWorktreesByProject);
+
+ React.useEffect(() => {
+ if (containerRef.current && !hasMeasured && !isExpanded) {
+ setCollapsedHeight(containerRef.current.offsetHeight);
+ setHasMeasured(true);
+ }
+ }, [hasMeasured, isExpanded]);
+
+ // Filter sessions by active project
+ const filteredSessions = React.useMemo(() => {
+ if (!activeProjectId) return sessions;
+
+ const activeProject = projects.find(p => p.id === activeProjectId);
+ if (!activeProject) return sessions;
+
+ const projectRoot = normalize(activeProject.path);
+ const projectDirs = new Set([projectRoot]);
+
+ // Add worktrees
+ const worktrees = availableWorktreesByProject.get(projectRoot) ?? [];
+ for (const meta of worktrees) {
+ const p = (meta && typeof meta === 'object' && 'path' in meta) ? (meta as { path?: unknown }).path : null;
+ if (typeof p === 'string' && p.trim()) {
+ const normalized = normalize(p);
+ if (normalized) projectDirs.add(normalized);
+ }
+ }
+
+ return sessions.filter(session => {
+ const sessionDir = normalize((session as { directory?: string | null }).directory ?? '');
+ return projectDirs.has(sessionDir);
+ });
+ }, [sessions, activeProjectId, projects, availableWorktreesByProject]);
+
+ const previewHeight = collapsedHeight ?? undefined;
+ const displaySessions = hasMeasured || isExpanded
+ ? filteredSessions.filter(s => s.id !== currentSessionId)
+ : filteredSessions.slice(0, 3);
+
+ return (
+
+ {/* Header row */}
+
+
+
+
+
{
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onToggleCollapse();
+ }
+ }}
+ >
+
+
+
+ {
+ e.stopPropagation();
+ onNewSession();
+ }}
+ className="flex items-center gap-0.5 px-2 py-1 text-[12px] leading-tight !min-h-0 rounded border border-[var(--primary-base)]/60 bg-[var(--primary-base)]/5 text-[var(--primary-base)]/80 hover:text-[var(--primary-base)] hover:bg-[var(--primary-base)]/10 self-start"
+ >
+ New
+
+
+
+
+ {/* Project switcher bar */}
+
+
+ {/* Sessions list */}
+
+ {displaySessions.length === 0 ? (
+
+ No sessions in this project
+
+ ) : (
+ displaySessions.map((session) => (
+
onSessionClick(session.id)}
+ onDoubleClick={onSessionDoubleClick}
+ needsAttention={needsAttention}
+ />
+ ))
+ )}
+
+
+ );
+}
+
+export const MobileSessionStatusBar: React.FC = ({
+ onSessionSwitch,
+ cornerRadius,
+}) => {
+ const { currentTheme } = useThemeSystem();
+ const sessions = useSessionStore((state) => state.sessions);
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+ const sessionStatus = useSessionStore((state) => state.sessionStatus);
+ const sessionAttentionStates = useSessionStore((state) => state.sessionAttentionStates);
+ const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
+ const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
+ const getContextUsage = useSessionStore((state) => state.getContextUsage);
+ const agents = useConfigStore((state) => state.agents);
+ const { getCurrentModel } = useConfigStore();
+ const { isMobile, showMobileSessionStatusBar, isMobileSessionStatusBarCollapsed, setIsMobileSessionStatusBarCollapsed } = useUIStore();
+ const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
+
+ // Project store
+ const projects = useProjectsStore((state) => state.projects);
+ const activeProjectId = useProjectsStore((state) => state.activeProjectId);
+ const setActiveProject = useProjectsStore((state) => state.setActiveProject);
+ const addProject = useProjectsStore((state) => state.addProject);
+ const removeProject = useProjectsStore((state) => state.removeProject);
+ const getActiveProject = useProjectsStore((state) => state.getActiveProject);
+
+ // Directory store
+ const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
+
+ const { sessions: sortedSessions, totalRunning, totalUnread, totalCount } = useSessionGrouping(sessions, sessionStatus, sessionAttentionStates);
+ const { getSessionAgentName, getSessionTitle, needsAttention } = useSessionHelpers(agents, sessionStatus, sessionAttentionStates);
+ const getProjectStatus = useProjectStatus(sessions, sessionStatus, sessionAttentionStates, currentSessionId);
+
+ const currentSession = sessions.find((s) => s.id === currentSessionId);
+ const currentSessionTitle = currentSession
+ ? getSessionTitle(currentSession)
+ : '← Swipe here to open sidebars →';
+
+ // Calculate current session's child indicators
+ const currentSessionWithStatus = sortedSessions.find((s) => s.id === currentSessionId);
+ const currentSessionChildIndicators = currentSessionWithStatus?._childIndicators ?? [];
+
+ const activeProject = getActiveProject();
+ const currentProjectLabel = activeProject?.label || formatDirectoryName(activeProject?.path || '', homeDirectory);
+ const currentProjectIcon = activeProject?.icon;
+ const currentProjectIconImageUrl = activeProject
+ ? getProjectIconImageUrl(activeProject, {
+ themeVariant: currentTheme.metadata.variant,
+ iconColor: currentTheme.colors.surface.foreground,
+ })
+ : null;
+ const currentProjectIconBackground = activeProject?.iconBackground ?? null;
+ const currentProjectColor = activeProject?.color;
+
+ // Calculate token usage for current session
+ const currentModel = getCurrentModel();
+ const limit = currentModel && typeof currentModel.limit === 'object' && currentModel.limit !== null
+ ? (currentModel.limit as Record)
+ : null;
+ const contextLimit = (limit && typeof limit.context === 'number' ? limit.context : 0);
+ const outputLimit = (limit && typeof limit.output === 'number' ? limit.output : 0);
+ const contextUsage = getContextUsage(contextLimit, outputLimit);
+
+ const [isExpanded, setIsExpanded] = React.useState(false);
+ const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []);
+
+ if (!isMobile || !showMobileSessionStatusBar || totalCount === 0) {
+ return null;
+ }
+
+ const handleSessionClick = (sessionId: string) => {
+ setCurrentSession(sessionId);
+ onSessionSwitch?.(sessionId);
+ setIsExpanded(false);
+ };
+
+ const handleSessionDoubleClick = () => {
+ // On double-tap, switch to the Chat tab
+ setActiveMainTab('chat');
+ };
+
+ const handleCreateSession = () => {
+ openNewSessionDraft();
+ };
+
+ const handleProjectSwitch = (projectId: string) => {
+ if (projectId !== activeProjectId) {
+ setActiveProject(projectId);
+ }
+ };
+
+ const handleAddProject = () => {
+ if (!tauriIpcAvailable || !isDesktopLocalOriginActive()) {
+ sessionEvents.requestDirectoryDialog();
+ return;
+ }
+ requestDirectoryAccess('')
+ .then((result) => {
+ if (result.success && result.path) {
+ const added = addProject(result.path, { id: result.projectId });
+ if (!added) {
+ toast.error('Failed to add project', {
+ description: 'Please select a valid directory.',
+ });
+ }
+ } else if (result.error && result.error !== 'Directory selection cancelled') {
+ toast.error('Failed to select directory', {
+ description: result.error,
+ });
+ }
+ })
+ .catch((error) => {
+ console.error('Failed to select directory:', error);
+ toast.error('Failed to select directory');
+ });
+ };
+
+ if (isMobileSessionStatusBarCollapsed) {
+ return (
+ setIsMobileSessionStatusBarCollapsed(false)}
+ onNewSession={handleCreateSession}
+ cornerRadius={cornerRadius}
+ contextUsage={contextUsage}
+ childIndicators={currentSessionChildIndicators}
+ />
+ );
+ }
+
+ return (
+ {
+ setIsMobileSessionStatusBarCollapsed(true);
+ setIsExpanded(false);
+ }}
+ onNewSession={handleCreateSession}
+ onSessionClick={handleSessionClick}
+ onSessionDoubleClick={handleSessionDoubleClick}
+ onProjectSwitch={handleProjectSwitch}
+ onAddProject={handleAddProject}
+ onRemoveProject={removeProject}
+ getSessionAgentName={getSessionAgentName}
+ getSessionTitle={getSessionTitle}
+ needsAttention={needsAttention}
+ cornerRadius={cornerRadius}
+ contextUsage={contextUsage}
+ projects={projects}
+ activeProjectId={activeProjectId}
+ getProjectStatus={getProjectStatus}
+ homeDirectory={homeDirectory}
+ childIndicators={currentSessionChildIndicators}
+ />
+ );
+};
diff --git a/ui/src/components/chat/ModelControls.tsx b/ui/src/components/chat/ModelControls.tsx
new file mode 100644
index 0000000..e09c0e3
--- /dev/null
+++ b/ui/src/components/chat/ModelControls.tsx
@@ -0,0 +1,2852 @@
+import React from 'react';
+import type { ComponentType } from 'react';
+import {
+ RiAddLine,
+ RiAiAgentLine,
+ RiArrowDownSLine,
+ RiArrowGoBackLine,
+ RiArrowRightSLine,
+ RiBrainAi3Line,
+ RiCheckLine,
+ RiCheckboxCircleLine,
+ RiCloseCircleLine,
+ RiFileImageLine,
+ RiFileMusicLine,
+ RiFilePdfLine,
+ RiFileVideoLine,
+ RiPencilAiLine,
+ RiQuestionLine,
+ RiSearchLine,
+ RiStarFill,
+ RiStarLine,
+ RiText,
+ RiTimeLine,
+ RiToolsLine,
+} from '@remixicon/react';
+import type { EditPermissionMode } from '@/stores/types/sessionTypes';
+import type { ModelMetadata } from '@/types';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { Input } from '@/components/ui/input';
+import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
+import { ProviderLogo } from '@/components/ui/ProviderLogo';
+import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
+import { Switch } from '@/components/ui/switch';
+import { TextLoop } from '@/components/ui/TextLoop';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
+import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
+import { isDesktopShell } from '@/lib/desktop';
+import { getAgentColor } from '@/lib/agentColors';
+import { useDeviceInfo } from '@/lib/device';
+import { getEditModeColors } from '@/lib/permissions/editModeColors';
+import { cn, fuzzyMatch } from '@/lib/utils';
+import { useContextStore } from '@/stores/contextStore';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { useModelLists } from '@/hooks/useModelLists';
+import { useIsTextTruncated } from '@/hooks/useIsTextTruncated';
+import type { MobileControlsPanel } from './mobileControlsUtils';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type IconComponent = ComponentType;
+
+type ProviderModel = Record & { id?: string; name?: string };
+
+type PermissionAction = 'allow' | 'ask' | 'deny';
+type PermissionRule = { permission: string; pattern: string; action: PermissionAction };
+
+const asPermissionRuleset = (value: unknown): PermissionRule[] | null => {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+ const rules: PermissionRule[] = [];
+ for (const entry of value) {
+ if (!entry || typeof entry !== 'object') {
+ continue;
+ }
+ const candidate = entry as Partial;
+ if (typeof candidate.permission !== 'string' || typeof candidate.pattern !== 'string' || typeof candidate.action !== 'string') {
+ continue;
+ }
+ if (candidate.action !== 'allow' && candidate.action !== 'ask' && candidate.action !== 'deny') {
+ continue;
+ }
+ rules.push({ permission: candidate.permission, pattern: candidate.pattern, action: candidate.action });
+ }
+ return rules;
+};
+
+const resolveWildcardPermissionAction = (ruleset: unknown, permission: string): PermissionAction | undefined => {
+ const rules = asPermissionRuleset(ruleset);
+ if (!rules || rules.length === 0) {
+ return undefined;
+ }
+
+ for (let i = rules.length - 1; i >= 0; i -= 1) {
+ const rule = rules[i];
+ if (rule.permission === permission && rule.pattern === '*') {
+ return rule.action;
+ }
+ }
+
+ for (let i = rules.length - 1; i >= 0; i -= 1) {
+ const rule = rules[i];
+ if (rule.permission === '*' && rule.pattern === '*') {
+ return rule.action;
+ }
+ }
+
+ return undefined;
+};
+
+interface CapabilityDefinition {
+ key: 'tool_call' | 'reasoning';
+ icon: IconComponent;
+ label: string;
+ isActive: (metadata?: ModelMetadata) => boolean;
+}
+
+const CAPABILITY_DEFINITIONS: CapabilityDefinition[] = [
+ {
+ key: 'tool_call',
+ icon: RiToolsLine,
+ label: 'Tool calling',
+ isActive: (metadata) => metadata?.tool_call === true,
+ },
+ {
+ key: 'reasoning',
+ icon: RiBrainAi3Line,
+ label: 'Reasoning',
+ isActive: (metadata) => metadata?.reasoning === true,
+ },
+];
+
+interface ModalityIconDefinition {
+ icon: IconComponent;
+ label: string;
+}
+
+type ModalityIcon = {
+ key: string;
+ icon: IconComponent;
+ label: string;
+};
+
+type ModelApplyResult = 'applied' | 'provider-missing' | 'model-missing';
+
+const MODALITY_ICON_MAP: Record = {
+ text: { icon: RiText, label: 'Text' },
+ image: { icon: RiFileImageLine, label: 'Image' },
+ video: { icon: RiFileVideoLine, label: 'Video' },
+ audio: { icon: RiFileMusicLine, label: 'Audio' },
+ pdf: { icon: RiFilePdfLine, label: 'PDF' },
+};
+
+const normalizeModality = (value: string) => value.trim().toLowerCase();
+
+const getModalityIcons = (metadata: ModelMetadata | undefined, direction: 'input' | 'output'): ModalityIcon[] => {
+ const modalityList = direction === 'input' ? metadata?.modalities?.input : metadata?.modalities?.output;
+ if (!Array.isArray(modalityList) || modalityList.length === 0) {
+ return [];
+ }
+
+ const uniqueValues = Array.from(new Set(modalityList.map((item) => normalizeModality(item))));
+
+ return uniqueValues
+ .map((modality) => {
+ const definition = MODALITY_ICON_MAP[modality];
+ if (!definition) {
+ return null;
+ }
+ return {
+ key: modality,
+ icon: definition.icon,
+ label: definition.label,
+ } satisfies ModalityIcon;
+ })
+ .filter((entry): entry is ModalityIcon => Boolean(entry));
+};
+
+const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', {
+ notation: 'compact',
+ compactDisplay: 'short',
+ maximumFractionDigits: 1,
+ minimumFractionDigits: 0,
+});
+
+const CURRENCY_FORMATTER = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ maximumFractionDigits: 4,
+ minimumFractionDigits: 2,
+});
+
+const ADD_PROVIDER_ID = '__add_provider__';
+
+const formatTokens = (value?: number | null) => {
+ if (typeof value !== 'number' || Number.isNaN(value)) {
+ return '—';
+ }
+
+ if (value === 0) {
+ return '0';
+ }
+
+ const formatted = COMPACT_NUMBER_FORMATTER.format(value);
+ return formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted;
+};
+
+const formatCost = (value?: number | null) => {
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
+ return '—';
+ }
+
+ return CURRENCY_FORMATTER.format(value);
+};
+
+const formatCompactPrice = (metadata?: ModelMetadata): string | null => {
+ if (!metadata?.cost) {
+ return null;
+ }
+
+ const inputCost = metadata.cost.input;
+ const outputCost = metadata.cost.output;
+ const hasInput = typeof inputCost === 'number' && Number.isFinite(inputCost);
+ const hasOutput = typeof outputCost === 'number' && Number.isFinite(outputCost);
+
+ if (hasInput && hasOutput) {
+ return `In ${formatCost(inputCost)} · Out ${formatCost(outputCost)}`;
+ }
+ if (hasInput) {
+ return `In ${formatCost(inputCost)}`;
+ }
+ if (hasOutput) {
+ return `Out ${formatCost(outputCost)}`;
+ }
+ return null;
+};
+
+const getCapabilityIcons = (metadata?: ModelMetadata) => {
+ return CAPABILITY_DEFINITIONS.filter((definition) => definition.isActive(metadata)).map((definition) => ({
+ key: definition.key,
+ icon: definition.icon,
+ label: definition.label,
+ }));
+};
+
+const formatKnowledge = (knowledge?: string) => {
+ if (!knowledge) {
+ return '—';
+ }
+
+ const match = knowledge.match(/^(\d{4})-(\d{2})$/);
+ if (match) {
+ const year = Number.parseInt(match[1], 10);
+ const monthIndex = Number.parseInt(match[2], 10) - 1;
+ const knowledgeDate = new Date(Date.UTC(year, monthIndex, 1));
+ if (!Number.isNaN(knowledgeDate.getTime())) {
+ return new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' }).format(knowledgeDate);
+ }
+ }
+
+ return knowledge;
+};
+
+const formatDate = (value?: string) => {
+ if (!value) {
+ return '—';
+ }
+
+ const parsedDate = new Date(value);
+ if (Number.isNaN(parsedDate.getTime())) {
+ return value;
+ }
+
+ return new Intl.DateTimeFormat('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ }).format(parsedDate);
+};
+
+interface ModelControlsProps {
+ className?: string;
+ mobilePanel?: MobileControlsPanel;
+ onMobilePanelChange?: (panel: MobileControlsPanel) => void;
+ onMobilePanelSelection?: () => void;
+ onAgentPanelSelection?: () => void;
+}
+
+export const ModelControls: React.FC = ({
+ className,
+ mobilePanel,
+ onMobilePanelChange,
+ onMobilePanelSelection,
+ onAgentPanelSelection,
+}) => {
+ const {
+ providers,
+ currentProviderId,
+ currentModelId,
+ currentVariant,
+ currentAgentName,
+ settingsDefaultVariant,
+ settingsDefaultAgent,
+ setProvider,
+ setSelectedProvider,
+ setModel,
+ setCurrentVariant,
+ getCurrentModelVariants,
+ setAgent,
+ getCurrentProvider,
+ getModelMetadata,
+ getCurrentAgent,
+ getVisibleAgents,
+ } = useConfigStore();
+
+ // Use visible agents (excludes hidden internal agents)
+ const agents = getVisibleAgents();
+ const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]);
+
+ const {
+ currentSessionId,
+ messages,
+ saveSessionAgentSelection,
+ saveAgentModelForSession,
+ getAgentModelForSession,
+ saveAgentModelVariantForSession,
+ getAgentModelVariantForSession,
+ analyzeAndSaveExternalSessionChoices,
+ } = useSessionStore();
+
+ const contextHydrated = useContextStore((state) => state.hasHydrated);
+
+ const sessionSavedAgentName = useContextStore((state) =>
+ currentSessionId ? state.sessionAgentSelections.get(currentSessionId) ?? null : null
+ );
+
+ const stickySessionAgentRef = React.useRef(null);
+ React.useEffect(() => {
+ if (!currentSessionId) {
+ stickySessionAgentRef.current = null;
+ return;
+ }
+ if (sessionSavedAgentName) {
+ stickySessionAgentRef.current = sessionSavedAgentName;
+ }
+ }, [currentSessionId, sessionSavedAgentName]);
+
+ const stickySessionAgentName = currentSessionId ? stickySessionAgentRef.current : null;
+
+ // Prefer per-session selection over global config to avoid flicker during server-driven mode switches.
+ const uiAgentName = currentSessionId
+ ? (sessionSavedAgentName || stickySessionAgentName || currentAgentName)
+ : currentAgentName;
+
+ const sessionIdForEditMode = currentSessionId ?? '__global__';
+ const sessionEditMode = useContextStore((state) => {
+ if (!uiAgentName) {
+ return undefined;
+ }
+ return state.getSessionAgentEditMode(sessionIdForEditMode, uiAgentName, 'ask');
+ });
+ const setSessionAgentEditMode = useContextStore((state) => state.setSessionAgentEditMode);
+ const {
+ toggleFavoriteModel,
+ isFavoriteModel,
+ collapsedModelProviders,
+ toggleModelProviderCollapsed,
+ addRecentModel,
+ addRecentAgent,
+ addRecentEffort,
+ isModelSelectorOpen,
+ setModelSelectorOpen,
+ setSettingsDialogOpen,
+ setSettingsPage,
+ } = useUIStore();
+ const hiddenModels = useUIStore((state) => state.hiddenModels);
+ const collapsedProviderSet = React.useMemo(
+ () => new Set(collapsedModelProviders.map((providerId) => providerId.trim()).filter(Boolean)),
+ [collapsedModelProviders]
+ );
+
+ // Separate state for agent selector to avoid conflict with model selector
+ const [isAgentSelectorOpen, setIsAgentSelectorOpen] = React.useState(false);
+ const { favoriteModelsList, recentModelsList } = useModelLists();
+
+ const { isMobile } = useDeviceInfo();
+ const isDesktop = React.useMemo(() => isDesktopShell(), []);
+ const isVSCodeRuntime = useIsVSCodeRuntime();
+ // Only use mobile panels on actual mobile devices, VSCode uses desktop dropdowns
+ const isCompact = isMobile;
+ const [localMobilePanel, setLocalMobilePanel] = React.useState(null);
+ const usingExternalMobilePanel = mobilePanel !== undefined && typeof onMobilePanelChange === 'function';
+ const activeMobilePanel = usingExternalMobilePanel ? mobilePanel : localMobilePanel;
+ const setActiveMobilePanel = usingExternalMobilePanel ? onMobilePanelChange : setLocalMobilePanel;
+ const [mobileTooltipOpen, setMobileTooltipOpen] = React.useState<'model' | 'agent' | null>(null);
+ const [mobileModelQuery, setMobileModelQuery] = React.useState('');
+ const manualVariantSelectionRef = React.useRef(false);
+ const closeMobilePanel = React.useCallback(() => setActiveMobilePanel(null), [setActiveMobilePanel]);
+ const closeMobileTooltip = React.useCallback(() => setMobileTooltipOpen(null), []);
+ const longPressTimerRef = React.useRef(undefined);
+ const [expandedMobileProviders, setExpandedMobileProviders] = React.useState>(() => {
+ const initial = new Set();
+ if (currentProviderId) {
+ initial.add(currentProviderId);
+ }
+ return initial;
+ });
+ // Use global state for model selector (allows Ctrl+M shortcut)
+ const agentMenuOpen = isModelSelectorOpen;
+ const setAgentMenuOpen = setModelSelectorOpen;
+ const openAddProviderSettings = React.useCallback(() => {
+ setSelectedProvider(ADD_PROVIDER_ID);
+ setSettingsPage('providers');
+ setSettingsDialogOpen(true);
+ setAgentMenuOpen(false);
+ closeMobilePanel();
+ }, [setSelectedProvider, setSettingsPage, setSettingsDialogOpen, setAgentMenuOpen, closeMobilePanel]);
+ const [desktopModelQuery, setDesktopModelQuery] = React.useState('');
+ const [modelSelectedIndex, setModelSelectedIndex] = React.useState(0);
+ const modelItemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
+
+ React.useEffect(() => {
+ if (activeMobilePanel === 'model') {
+ setExpandedMobileProviders(() => {
+ const initial = new Set();
+ if (currentProviderId) {
+ initial.add(currentProviderId);
+ }
+ return initial;
+ });
+ }
+ }, [activeMobilePanel, currentProviderId]);
+
+ React.useEffect(() => {
+ if (activeMobilePanel !== 'model') {
+ setMobileModelQuery('');
+ }
+ }, [activeMobilePanel]);
+
+ // Handle model selector close behavior (separate from agent selector)
+ const prevModelSelectorOpenRef = React.useRef(isModelSelectorOpen);
+ React.useEffect(() => {
+ const wasOpen = prevModelSelectorOpenRef.current;
+ prevModelSelectorOpenRef.current = isModelSelectorOpen;
+
+ if (!isModelSelectorOpen) {
+ setDesktopModelQuery('');
+ setModelSelectedIndex(0);
+
+ // Restore focus to chat input when model selector closes
+ if (wasOpen && !isCompact) {
+ requestAnimationFrame(() => {
+ const textarea = document.querySelector('textarea[data-chat-input="true"]');
+ textarea?.focus();
+ });
+ }
+ }
+ }, [isModelSelectorOpen, isCompact]);
+
+ // Handle agent selector close behavior
+ const [agentSearchQuery, setAgentSearchQuery] = React.useState('');
+ React.useEffect(() => {
+ if (!isAgentSelectorOpen) {
+ setAgentSearchQuery('');
+ if (!isCompact) {
+ requestAnimationFrame(() => {
+ const textarea = document.querySelector('textarea[data-chat-input="true"]');
+ textarea?.focus();
+ });
+ }
+ }
+ }, [isAgentSelectorOpen, isCompact]);
+
+ // Reset selected index when search query changes
+ React.useEffect(() => {
+ setModelSelectedIndex(0);
+ }, [desktopModelQuery]);
+
+ const selectableDesktopAgents = React.useMemo(() => {
+ return agents.filter((agent) => agent.mode !== 'subagent');
+ }, [agents]);
+
+ const sortedAndFilteredAgents = React.useMemo(() => {
+ const sorted = [...selectableDesktopAgents].sort((a, b) => a.name.localeCompare(b.name));
+ if (!agentSearchQuery.trim()) {
+ return sorted;
+ }
+ return sorted.filter((agent) =>
+ fuzzyMatch(agent.name, agentSearchQuery) ||
+ (agent.description && fuzzyMatch(agent.description, agentSearchQuery))
+ );
+ }, [selectableDesktopAgents, agentSearchQuery]);
+
+ const defaultAgentName = React.useMemo(() => {
+ if (settingsDefaultAgent) {
+ const found = selectableDesktopAgents.find(a => a.name === settingsDefaultAgent);
+ if (found) return found.name;
+ }
+ const buildAgent = selectableDesktopAgents.find(a => a.name === 'build');
+ if (buildAgent) return buildAgent.name;
+ return selectableDesktopAgents[0]?.name;
+ }, [settingsDefaultAgent, selectableDesktopAgents]);
+
+ const currentAgent = React.useMemo(() => {
+ if (uiAgentName) {
+ return agents.find((agent) => agent.name === uiAgentName);
+ }
+ return getCurrentAgent?.();
+ }, [agents, getCurrentAgent, uiAgentName]);
+
+ const agentEditAction = React.useMemo(() => {
+ if (!currentAgent) {
+ return 'deny';
+ }
+ return resolveWildcardPermissionAction(currentAgent.permission, 'edit') ?? 'allow';
+ }, [currentAgent]);
+
+ const selectionContextReady = Boolean(uiAgentName);
+
+ const approveEditsAvailable = agentEditAction === 'ask';
+ const approveEditsChecked = approveEditsAvailable
+ ? sessionEditMode === 'allow' || sessionEditMode === 'full'
+ : agentEditAction === 'allow';
+ const approveEditsDisabled = !selectionContextReady || !approveEditsAvailable;
+
+ const sizeVariant: 'mobile' | 'vscode' | 'default' = isMobile ? 'mobile' : isVSCodeRuntime ? 'vscode' : 'default';
+ const buttonHeight = sizeVariant === 'mobile' ? 'h-9' : sizeVariant === 'vscode' ? 'h-6' : 'h-8';
+ const editToggleIconClass = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-4';
+ const controlIconSize = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-4';
+ const controlTextSize = isCompact ? 'typography-micro' : 'typography-meta';
+ const inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-1' : sizeVariant === 'vscode' ? 'gap-x-2' : 'gap-x-3';
+ const renderEditModeIcon = React.useCallback((mode: EditPermissionMode, iconClass = editToggleIconClass) => {
+ const combinedClassName = cn(iconClass, 'flex-shrink-0');
+ const modeColors = getEditModeColors(mode);
+ const iconColor = modeColors ? modeColors.text : 'var(--foreground)';
+ const iconStyle = { color: iconColor };
+
+ if (mode === 'full') {
+ return ;
+ }
+ if (mode === 'allow') {
+ return ;
+ }
+ if (mode === 'deny') {
+ return ;
+ }
+ return ;
+ }, [editToggleIconClass]);
+
+ const handleApproveEditsToggle = React.useCallback((checked: boolean) => {
+ if (!selectionContextReady || !currentAgentName || !approveEditsAvailable) {
+ return;
+ }
+ setSessionAgentEditMode(sessionIdForEditMode, currentAgentName, checked ? 'allow' : 'ask', 'ask');
+ }, [approveEditsAvailable, currentAgentName, selectionContextReady, setSessionAgentEditMode, sessionIdForEditMode]);
+
+ const currentProvider = getCurrentProvider();
+ const models = Array.isArray(currentProvider?.models) ? currentProvider.models : [];
+
+ const visibleProviders = React.useMemo(() => {
+ return providers
+ .map((provider) => {
+ const providerModels = Array.isArray(provider.models) ? provider.models : [];
+ const visibleModels = providerModels.filter((model: ProviderModel) => {
+ const modelId = typeof model?.id === 'string' ? model.id : '';
+ return !hiddenModels.some(
+ (item) => item.providerID === String(provider.id) && item.modelID === modelId
+ );
+ });
+ return { ...provider, models: visibleModels };
+ })
+ .filter((provider) => provider.models.length > 0);
+ }, [providers, hiddenModels]);
+
+ const currentMetadata =
+ currentProviderId && currentModelId ? getModelMetadata(currentProviderId, currentModelId) : undefined;
+ const currentCapabilityIcons = getCapabilityIcons(currentMetadata);
+ const inputModalityIcons = getModalityIcons(currentMetadata, 'input');
+ const outputModalityIcons = getModalityIcons(currentMetadata, 'output');
+
+ // Compute from current model each render to avoid stale variants
+ // in draft/session transitions.
+ const availableVariants = getCurrentModelVariants();
+ const hasVariants = availableVariants.length > 0;
+
+ const costRows = [
+ { label: 'Input', value: formatCost(currentMetadata?.cost?.input) },
+ { label: 'Output', value: formatCost(currentMetadata?.cost?.output) },
+ { label: 'Cache read', value: formatCost(currentMetadata?.cost?.cache_read) },
+ { label: 'Cache write', value: formatCost(currentMetadata?.cost?.cache_write) },
+ ];
+
+ const limitRows = [
+ { label: 'Context', value: formatTokens(currentMetadata?.limit?.context) },
+ { label: 'Output', value: formatTokens(currentMetadata?.limit?.output) },
+ ];
+
+ const prevAgentNameRef = React.useRef(undefined);
+
+ const currentSessionMessageCount = currentSessionId ? (messages.get(currentSessionId)?.length ?? -1) : -1;
+
+ const sessionInitializationRef = React.useRef<{
+ sessionId: string;
+ resolved: boolean;
+ inFlight: boolean;
+ } | null>(null);
+
+ // If we have an explicit per-session agent selection (eg. server-injected mode switch),
+ // treat the session as resolved and don't run inference/fallback that could cause flicker.
+ React.useEffect(() => {
+ if (!currentSessionId) {
+ return;
+ }
+ const refState = sessionInitializationRef.current;
+ if (!refState || refState.sessionId !== currentSessionId) {
+ return;
+ }
+
+ if (sessionSavedAgentName && agents.some((agent) => agent.name === sessionSavedAgentName)) {
+ refState.resolved = true;
+ refState.inFlight = false;
+ }
+ }, [agents, currentSessionId, sessionSavedAgentName]);
+
+ const tryApplyModelSelection = React.useCallback(
+ (providerId: string, modelId: string, agentName?: string): ModelApplyResult => {
+ if (!providerId || !modelId) {
+ return 'model-missing';
+ }
+
+ const provider = providers.find(p => p.id === providerId);
+ if (!provider) {
+ return 'provider-missing';
+ }
+
+ const providerModels = Array.isArray(provider.models) ? provider.models : [];
+ const modelExists = providerModels.find((m: ProviderModel) => m.id === modelId);
+ if (!modelExists) {
+ return 'model-missing';
+ }
+
+ setProvider(providerId);
+ setModel(modelId);
+
+ if (currentSessionId && agentName) {
+ saveAgentModelForSession(currentSessionId, agentName, providerId, modelId);
+ }
+
+ return 'applied';
+ },
+ [providers, setProvider, setModel, currentSessionId, saveAgentModelForSession],
+ );
+
+ React.useEffect(() => {
+ if (!currentSessionId) {
+ sessionInitializationRef.current = null;
+ return;
+ }
+
+ if (!contextHydrated || providers.length === 0 || agents.length === 0) {
+ return;
+ }
+
+ if (!sessionInitializationRef.current || sessionInitializationRef.current.sessionId !== currentSessionId) {
+ sessionInitializationRef.current = { sessionId: currentSessionId, resolved: false, inFlight: false };
+ }
+
+ const state = sessionInitializationRef.current;
+ if (!state || state.resolved || state.inFlight) {
+ return;
+ }
+
+ let isCancelled = false;
+
+ const finalize = () => {
+ if (isCancelled) {
+ return;
+ }
+ const refState = sessionInitializationRef.current;
+ if (refState && refState.sessionId === currentSessionId) {
+ refState.resolved = true;
+ refState.inFlight = false;
+ }
+ };
+
+ const applySavedSelections = (): 'resolved' | 'waiting' | 'continue' => {
+ const savedAgentName = currentSessionId
+ ? (useContextStore.getState().getSessionAgentSelection(currentSessionId) || stickySessionAgentRef.current)
+ : null;
+ if (savedAgentName) {
+ if (currentAgentName !== savedAgentName) {
+ setAgent(savedAgentName);
+ }
+
+ const savedModel = getAgentModelForSession(currentSessionId, savedAgentName);
+ if (savedModel) {
+ const result = tryApplyModelSelection(savedModel.providerId, savedModel.modelId, savedAgentName);
+ if (result === 'applied') {
+ return 'resolved';
+ }
+ if (result === 'provider-missing') {
+ return 'waiting';
+ }
+ } else {
+ return 'resolved';
+ }
+ }
+
+ for (const agent of agents) {
+ const selection = getAgentModelForSession(currentSessionId, agent.name);
+ if (!selection) {
+ continue;
+ }
+
+ if (currentAgentName !== agent.name) {
+ setAgent(agent.name);
+ }
+
+ const existingSelection = useContextStore.getState().getSessionAgentSelection(currentSessionId) || stickySessionAgentRef.current;
+ if (!existingSelection) {
+ saveSessionAgentSelection(currentSessionId, agent.name);
+ }
+ const result = tryApplyModelSelection(selection.providerId, selection.modelId, agent.name);
+ if (result === 'applied') {
+ return 'resolved';
+ }
+ if (result === 'provider-missing') {
+ return 'waiting';
+ }
+ }
+
+ return 'continue';
+ };
+
+ const applyFallbackAgent = () => {
+ if (agents.length === 0) {
+ return;
+ }
+
+ const existingSelection = currentSessionId
+ ? (useContextStore.getState().getSessionAgentSelection(currentSessionId) || stickySessionAgentRef.current)
+ : null;
+
+ // If we already have a valid agent selected (often from server-injected mode switch),
+ // don't override it with a fallback.
+ const preferred =
+ (currentSessionId
+ ? (useContextStore.getState().getSessionAgentSelection(currentSessionId) || stickySessionAgentRef.current)
+ : null) ||
+ currentAgentName;
+ if (preferred && agents.some((agent) => agent.name === preferred)) {
+ if (currentAgentName !== preferred) {
+ setAgent(preferred);
+ }
+ return;
+ }
+
+ const fallbackAgent = agents.find(agent => agent.name === 'build') || primaryAgents[0] || agents[0];
+ if (!fallbackAgent) {
+ return;
+ }
+
+ if (!existingSelection) {
+ saveSessionAgentSelection(currentSessionId, fallbackAgent.name);
+ }
+
+ if (currentAgentName !== fallbackAgent.name) {
+ setAgent(fallbackAgent.name);
+ }
+
+ if (fallbackAgent.model?.providerID && fallbackAgent.model?.modelID) {
+ tryApplyModelSelection(fallbackAgent.model.providerID, fallbackAgent.model.modelID, fallbackAgent.name);
+ }
+ };
+
+ const resolveSessionPreferences = async () => {
+ try {
+ const savedOutcome = applySavedSelections();
+ if (savedOutcome === 'resolved') {
+ finalize();
+ return;
+ }
+ if (savedOutcome === 'waiting') {
+ return;
+ }
+
+ if (currentSessionMessageCount === -1) {
+ return;
+ }
+
+ if (currentSessionMessageCount > 0) {
+ state.inFlight = true;
+ try {
+ const discoveredChoices = await analyzeAndSaveExternalSessionChoices(currentSessionId, agents);
+ if (isCancelled) {
+ return;
+ }
+
+ if (discoveredChoices.size > 0) {
+ let latestAgent: string | null = null;
+ let latestTimestamp = -Infinity;
+
+ for (const [agentName, choice] of discoveredChoices) {
+ if (choice.timestamp > latestTimestamp) {
+ latestTimestamp = choice.timestamp;
+ latestAgent = agentName;
+ }
+ }
+
+ if (latestAgent) {
+ // If server/user already selected an agent for this session, don't override
+ // with heuristic inference mid-stream.
+ const latestSaved = useContextStore.getState().getSessionAgentSelection(currentSessionId) || stickySessionAgentRef.current;
+ if (latestSaved && latestSaved !== latestAgent) {
+ finalize();
+ return;
+ }
+
+ if (!latestSaved) {
+ saveSessionAgentSelection(currentSessionId, latestAgent);
+ }
+ if (currentAgentName !== latestAgent) {
+ setAgent(latestAgent);
+ }
+
+ const latestChoice = discoveredChoices.get(latestAgent);
+ if (latestChoice) {
+ const applyResult = tryApplyModelSelection(
+ latestChoice.providerId,
+ latestChoice.modelId,
+ latestAgent,
+ );
+
+ if (applyResult === 'applied') {
+ finalize();
+ return;
+ }
+
+ if (applyResult === 'provider-missing') {
+ return;
+ }
+ } else {
+ finalize();
+ return;
+ }
+ }
+ }
+ } catch (error) {
+ if (!isCancelled) {
+ console.error('[ModelControls] Error resolving session from messages:', error);
+ }
+ } finally {
+ const refState = sessionInitializationRef.current;
+ if (!isCancelled && refState && refState.sessionId === currentSessionId) {
+ refState.inFlight = false;
+ }
+ }
+ }
+
+ if (isCancelled) {
+ return;
+ }
+
+ applyFallbackAgent();
+ finalize();
+ } catch (error) {
+ if (!isCancelled) {
+ console.error('[ModelControls] Error in session switch:', error);
+ }
+ }
+ };
+
+ resolveSessionPreferences();
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [
+ currentSessionId,
+ currentSessionMessageCount,
+ agents,
+ primaryAgents,
+ currentAgentName,
+ getAgentModelForSession,
+ setAgent,
+ tryApplyModelSelection,
+ analyzeAndSaveExternalSessionChoices,
+ saveSessionAgentSelection,
+ contextHydrated,
+ providers,
+ sessionSavedAgentName,
+ ]);
+
+ React.useEffect(() => {
+ if (!contextHydrated || !currentSessionId || providers.length === 0 || agents.length === 0) {
+ return;
+ }
+
+ const preferredAgent = sessionSavedAgentName || currentAgentName;
+ if (!preferredAgent) {
+ return;
+ }
+
+ const preferredSelection = getAgentModelForSession(currentSessionId, preferredAgent);
+ if (!preferredSelection) {
+ return;
+ }
+
+ const provider = providers.find(p => p.id === preferredSelection.providerId);
+ if (!provider) {
+ return;
+ }
+
+ const modelExists = Array.isArray(provider.models)
+ ? provider.models.some((m: ProviderModel) => m.id === preferredSelection.modelId)
+ : false;
+ if (!modelExists) {
+ return;
+ }
+
+ const providerMatches = currentProviderId === preferredSelection.providerId;
+ const modelMatches = currentModelId === preferredSelection.modelId;
+ if (providerMatches && modelMatches) {
+ return;
+ }
+
+ if (preferredAgent !== currentAgentName) {
+ setAgent(preferredAgent);
+ }
+
+ tryApplyModelSelection(preferredSelection.providerId, preferredSelection.modelId, preferredAgent);
+ }, [
+ contextHydrated,
+ currentSessionId,
+ currentAgentName,
+ currentProviderId,
+ currentModelId,
+ providers,
+ agents,
+ getAgentModelForSession,
+ tryApplyModelSelection,
+ setAgent,
+ sessionSavedAgentName,
+ ]);
+
+ React.useEffect(() => {
+ if (!contextHydrated) {
+ return;
+ }
+
+ const handleAgentSwitch = async () => {
+ try {
+ if (currentAgentName !== prevAgentNameRef.current) {
+ prevAgentNameRef.current = currentAgentName;
+
+ if (currentAgentName && currentSessionId) {
+ await new Promise(resolve => setTimeout(resolve, 50));
+
+ const persistedChoice = getAgentModelForSession(currentSessionId, currentAgentName);
+
+ if (persistedChoice) {
+ const result = tryApplyModelSelection(
+ persistedChoice.providerId,
+ persistedChoice.modelId,
+ currentAgentName,
+ );
+ if (result === 'applied' || result === 'provider-missing') {
+ return;
+ }
+ }
+
+ const agent = agents.find(a => a.name === currentAgentName);
+ if (agent?.model?.providerID && agent?.model?.modelID) {
+ const result = tryApplyModelSelection(
+ agent.model.providerID,
+ agent.model.modelID,
+ currentAgentName,
+ );
+ if (result === 'provider-missing') {
+ return;
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.error('[ModelControls] Agent change error:', error);
+ }
+ };
+
+ handleAgentSwitch();
+ }, [currentAgentName, currentSessionId, getAgentModelForSession, tryApplyModelSelection, agents, contextHydrated]);
+
+ React.useEffect(() => {
+ if (!contextHydrated || !currentAgentName) {
+ manualVariantSelectionRef.current = false;
+ setCurrentVariant(undefined);
+ return;
+ }
+
+ if (!currentProviderId || !currentModelId) {
+ manualVariantSelectionRef.current = false;
+ setCurrentVariant(undefined);
+ return;
+ }
+
+ if (availableVariants.length === 0) {
+ manualVariantSelectionRef.current = false;
+ setCurrentVariant(undefined);
+ return;
+ }
+
+ if (currentVariant && !availableVariants.includes(currentVariant)) {
+ setCurrentVariant(undefined);
+ return;
+ }
+
+ // Draft state (no session yet): seed from settings default, but don't override
+ // user selection while drafting.
+ if (!currentSessionId) {
+ if (!currentVariant && !manualVariantSelectionRef.current) {
+ const desired = settingsDefaultVariant && availableVariants.includes(settingsDefaultVariant)
+ ? settingsDefaultVariant
+ : undefined;
+ setCurrentVariant(desired);
+ }
+ return;
+ }
+
+ const savedVariant = getAgentModelVariantForSession(
+ currentSessionId,
+ currentAgentName,
+ currentProviderId,
+ currentModelId,
+ );
+
+ const resolvedSaved = savedVariant && availableVariants.includes(savedVariant)
+ ? savedVariant
+ : undefined;
+
+ setCurrentVariant(resolvedSaved);
+ manualVariantSelectionRef.current = false;
+ }, [
+ availableVariants,
+ contextHydrated,
+ currentSessionId,
+ currentAgentName,
+ currentProviderId,
+ currentModelId,
+ currentVariant,
+ getAgentModelVariantForSession,
+ setCurrentVariant,
+ settingsDefaultVariant,
+ ]);
+
+ React.useEffect(() => {
+ manualVariantSelectionRef.current = false;
+ }, [currentProviderId, currentModelId]);
+
+ const handleVariantSelect = React.useCallback((variant: string | undefined) => {
+ manualVariantSelectionRef.current = true;
+ setCurrentVariant(variant);
+
+ if (currentProviderId && currentModelId) {
+ addRecentEffort(currentProviderId, currentModelId, variant);
+ }
+
+ if (currentSessionId && currentAgentName && currentProviderId && currentModelId) {
+ saveAgentModelVariantForSession(
+ currentSessionId,
+ currentAgentName,
+ currentProviderId,
+ currentModelId,
+ variant,
+ );
+ }
+ }, [
+ addRecentEffort,
+ currentAgentName,
+ currentModelId,
+ currentProviderId,
+ currentSessionId,
+ saveAgentModelVariantForSession,
+ setCurrentVariant,
+ ]);
+
+ const handleAgentChange = (agentName: string) => {
+ try {
+ setAgent(agentName);
+ addRecentAgent(agentName);
+ setAgentMenuOpen(false);
+
+ if (currentSessionId) {
+ saveSessionAgentSelection(currentSessionId, agentName);
+ }
+ if (isCompact) {
+ closeMobilePanel();
+ const callback = onAgentPanelSelection || onMobilePanelSelection;
+ if (callback) {
+ requestAnimationFrame(() => {
+ callback();
+ });
+ }
+ }
+ } catch (error) {
+ console.error('[ModelControls] Handle agent change error:', error);
+ }
+ };
+
+ const handleProviderAndModelChange = (providerId: string, modelId: string) => {
+ try {
+ const result = tryApplyModelSelection(providerId, modelId, currentAgentName || undefined);
+ if (result !== 'applied') {
+ if (result === 'provider-missing') {
+ console.error('[ModelControls] Provider not available for selection:', providerId);
+ } else if (result === 'model-missing') {
+ console.error('[ModelControls] Model not available for selection:', { providerId, modelId });
+ }
+ return;
+ }
+ // Add to recent models on successful selection
+ addRecentModel(providerId, modelId);
+ setAgentMenuOpen(false);
+ if (isCompact) {
+ closeMobilePanel();
+ if (onMobilePanelSelection) {
+ requestAnimationFrame(() => {
+ onMobilePanelSelection();
+ });
+ }
+ }
+ if (!isCompact || !onMobilePanelSelection) {
+ // Restore focus to chat input after model selection
+ requestAnimationFrame(() => {
+ const textarea = document.querySelector('textarea[data-chat-input="true"]');
+ textarea?.focus();
+ });
+ }
+ } catch (error) {
+ console.error('[ModelControls] Handle model change error:', error);
+ }
+ };
+
+ const getModelDisplayName = (model: ProviderModel | undefined) => {
+ const name = (typeof model?.name === 'string' ? model.name : (typeof model?.id === 'string' ? model.id : ''));
+ if (name.length > 40) {
+ return name.substring(0, 37) + '...';
+ }
+ return name;
+ };
+
+ const getProviderDisplayName = () => {
+ const provider = providers.find(p => p.id === currentProviderId);
+ return provider?.name || currentProviderId;
+ };
+
+ const getCurrentModelDisplayName = () => {
+ if (!currentProviderId || !currentModelId) return 'Not selected';
+ if (models.length === 0) return 'Not selected';
+ const currentModel = models.find((m: ProviderModel) => m.id === currentModelId);
+ return getModelDisplayName(currentModel);
+ };
+
+ const currentModelDisplayName = getCurrentModelDisplayName();
+ const modelLabelRef = React.useRef(null);
+ const isModelLabelTruncated = useIsTextTruncated(modelLabelRef, [currentModelDisplayName, isCompact]);
+
+ const getAgentDisplayName = () => {
+ if (!uiAgentName) {
+ const buildAgent = primaryAgents.find(agent => agent.name === 'build');
+ const defaultAgent = buildAgent || primaryAgents[0];
+ return defaultAgent ? capitalizeAgentName(defaultAgent.name) : 'Select Agent';
+ }
+ const agent = agents.find(a => a.name === uiAgentName);
+ return agent ? capitalizeAgentName(agent.name) : capitalizeAgentName(uiAgentName);
+ };
+
+ const capitalizeAgentName = (name: string) => {
+ return name.charAt(0).toUpperCase() + name.slice(1);
+ };
+
+ const renderIconBadge = (IconComp: IconComponent, label: string, key: string) => (
+
+
+
+ );
+
+ const toggleMobileProviderExpansion = React.useCallback((providerId: string) => {
+ setExpandedMobileProviders((prev) => {
+ const next = new Set(prev);
+ if (next.has(providerId)) {
+ next.delete(providerId);
+ } else {
+ next.add(providerId);
+ }
+ return next;
+ });
+ }, []);
+
+ const handleLongPressStart = React.useCallback((type: 'model' | 'agent') => {
+ if (longPressTimerRef.current) {
+ clearTimeout(longPressTimerRef.current);
+ }
+ longPressTimerRef.current = setTimeout(() => {
+ setMobileTooltipOpen(type);
+ }, 500);
+ }, []);
+
+ const handleLongPressEnd = React.useCallback(() => {
+ if (longPressTimerRef.current) {
+ clearTimeout(longPressTimerRef.current);
+ }
+ }, []);
+
+ React.useEffect(() => {
+ return () => {
+ if (longPressTimerRef.current) {
+ clearTimeout(longPressTimerRef.current);
+ }
+ };
+ }, []);
+
+ const renderMobileModelTooltip = () => {
+ if (!isCompact || mobileTooltipOpen !== 'model') return null;
+
+ return (
+
+
+ {}
+
+
Provider
+
{getProviderDisplayName()}
+
+
+ {}
+ {currentCapabilityIcons.length > 0 && (
+
+
Capabilities
+
+ {currentCapabilityIcons.map(({ key, icon, label }) => (
+
+ {renderIconBadge(icon, label, `cap-${key}`)}
+ {label}
+
+ ))}
+
+
+ )}
+
+ {}
+ {(inputModalityIcons.length > 0 || outputModalityIcons.length > 0) && (
+
+
Modalities
+
+ {inputModalityIcons.length > 0 && (
+
+
Input
+
+ {inputModalityIcons.map(({ key, icon, label }) => renderIconBadge(icon, `${label} input`, `input-${key}`))}
+
+
+ )}
+ {outputModalityIcons.length > 0 && (
+
+
Output
+
+ {outputModalityIcons.map(({ key, icon, label }) => renderIconBadge(icon, `${label} output`, `output-${key}`))}
+
+
+ )}
+
+
+ )}
+
+ {}
+
+
Limits
+
+
+ Context
+ {formatTokens(currentMetadata?.limit?.context)}
+
+
+ Output
+ {formatTokens(currentMetadata?.limit?.output)}
+
+
+
+
+ {}
+
+
Metadata
+
+
+ Knowledge
+ {formatKnowledge(currentMetadata?.knowledge)}
+
+
+ Release
+ {formatDate(currentMetadata?.release_date)}
+
+
+
+
+
+ );
+ };
+
+ const renderMobileAgentTooltip = () => {
+ if (!isCompact || mobileTooltipOpen !== 'agent' || !currentAgent) return null;
+
+ const hasCustomPrompt = Boolean(currentAgent.prompt && currentAgent.prompt.trim().length > 0);
+ const hasModelConfig = currentAgent.model?.providerID && currentAgent.model?.modelID;
+ const hasTemperatureOrTopP = currentAgent.temperature !== undefined || currentAgent.topP !== undefined;
+
+ const summarizePermission = (permissionName: string): { mode: EditPermissionMode; label: string } => {
+ const rules = asPermissionRuleset(currentAgent.permission) ?? [];
+ const hasCustom = rules.some((rule) => rule.permission === permissionName && rule.pattern !== '*');
+ const action = resolveWildcardPermissionAction(rules, permissionName) ?? 'ask';
+
+ if (hasCustom) {
+ return { mode: 'ask', label: 'Custom' };
+ }
+
+ if (action === 'allow') return { mode: 'allow', label: 'Allow' };
+ if (action === 'deny') return { mode: 'deny', label: 'Deny' };
+ return { mode: 'ask', label: 'Ask' };
+ };
+
+ const editPermissionSummary = summarizePermission('edit');
+ const bashPermissionSummary = summarizePermission('bash');
+ const webfetchPermissionSummary = summarizePermission('webfetch');
+
+ return (
+
+
+ {}
+ {currentAgent.description && (
+
+
{currentAgent.description}
+
+ )}
+
+ {}
+
+
Mode
+
+ {currentAgent.mode === 'primary' ? 'Primary' : currentAgent.mode === 'subagent' ? 'Subagent' : currentAgent.mode === 'all' ? 'All' : '—'}
+
+
+
+ {}
+ {(hasModelConfig || hasTemperatureOrTopP) && (
+
+
Model
+ {hasModelConfig && (
+
+ {currentAgent.model!.providerID} / {currentAgent.model!.modelID}
+
+ )}
+ {hasTemperatureOrTopP && (
+
+ {currentAgent.temperature !== undefined && (
+
+ Temperature
+ {currentAgent.temperature}
+
+ )}
+ {currentAgent.topP !== undefined && (
+
+ Top P
+ {currentAgent.topP}
+
+ )}
+
+ )}
+
+ )}
+
+
+ {}
+
+
Permissions
+
+
+
Edit
+
+ {renderEditModeIcon(editPermissionSummary.mode, 'h-3.5 w-3.5')}
+
+ {editPermissionSummary.label}
+
+
+
+
+
Bash
+
+ {renderEditModeIcon(bashPermissionSummary.mode, 'h-3.5 w-3.5')}
+
+ {bashPermissionSummary.label}
+
+
+
+
+
WebFetch
+
+ {renderEditModeIcon(webfetchPermissionSummary.mode, 'h-3.5 w-3.5')}
+
+ {webfetchPermissionSummary.label}
+
+
+
+
+
+
+ {}
+ {hasCustomPrompt && (
+
+ )}
+
+
+ );
+ };
+
+ const normalizeModelSearchValue = React.useCallback((value: string) => {
+ const lower = value.toLowerCase().trim();
+ const compact = lower.replace(/[^a-z0-9]/g, '');
+ const tokens = lower.split(/[^a-z0-9]+/).filter(Boolean);
+ return { lower, compact, tokens };
+ }, []);
+
+ const matchesModelSearch = React.useCallback((candidate: string, query: string) => {
+ const normalizedQuery = normalizeModelSearchValue(query);
+ if (!normalizedQuery.lower) {
+ return true;
+ }
+
+ const normalizedCandidate = normalizeModelSearchValue(candidate);
+ if (normalizedCandidate.lower.includes(normalizedQuery.lower)) {
+ return true;
+ }
+
+ if (normalizedQuery.compact.length >= 2 && normalizedCandidate.compact.includes(normalizedQuery.compact)) {
+ return true;
+ }
+
+ if (normalizedQuery.tokens.length === 0) {
+ return false;
+ }
+
+ return normalizedQuery.tokens.every((queryToken) =>
+ normalizedCandidate.tokens.some((candidateToken) =>
+ candidateToken.startsWith(queryToken) || candidateToken.includes(queryToken)
+ )
+ );
+ }, [normalizeModelSearchValue]);
+
+ const renderMobileModelPanel = () => {
+ if (!isCompact) return null;
+
+ const normalizedQuery = mobileModelQuery.trim();
+ const filteredProviders = visibleProviders
+ .map((provider) => {
+ const providerModels = Array.isArray(provider.models) ? provider.models : [];
+ const matchesProvider = normalizedQuery.length === 0
+ ? true
+ : matchesModelSearch(provider.name, normalizedQuery) || matchesModelSearch(provider.id, normalizedQuery);
+ const matchingModels = normalizedQuery.length === 0
+ ? providerModels
+ : providerModels.filter((model: ProviderModel) => {
+ const name = getModelDisplayName(model);
+ const id = typeof model.id === 'string' ? model.id : '';
+ return matchesModelSearch(name, normalizedQuery) || matchesModelSearch(id, normalizedQuery);
+ });
+ return { provider, providerModels: matchingModels, matchesProvider };
+ })
+ .filter(({ matchesProvider, providerModels }) => matchesProvider || providerModels.length > 0);
+
+ return (
+
+
+
+
+
+ setMobileModelQuery(event.target.value)}
+ placeholder="Search providers or models"
+ className="pl-7 h-9 rounded-xl border-border/40 bg-[var(--surface-elevated)] typography-meta"
+ />
+ {mobileModelQuery && (
+ setMobileModelQuery('')}
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
+ aria-label="Clear search"
+ >
+
+
+ )}
+
+
+
+ {filteredProviders.length === 0 && (
+
+ No providers or models match your search.
+
+ )}
+
+ {/* Favorites Section for Mobile */}
+ {!mobileModelQuery && favoriteModelsList.length > 0 && (
+
+
+
+ Favorites
+
+
+ {favoriteModelsList.map(({ model, providerID, modelID }) => {
+ const isSelected = providerID === currentProviderId && modelID === currentModelId;
+ const metadata = getModelMetadata(providerID, modelID);
+
+ return (
+
handleProviderAndModelChange(providerID, modelID)}
+ className={cn(
+ 'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 text-left last:border-b-0',
+ 'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
+ 'first:rounded-t-xl last:rounded-b-xl transition-colors',
+ isSelected ? 'bg-interactive-selection/15 text-interactive-selection-foreground' : 'hover:bg-interactive-hover'
+ )}
+ >
+
+
+
+ {getModelDisplayName(model)}
+
+
+
+ {(metadata?.limit?.context || metadata?.limit?.output) && (
+
+ {metadata?.limit?.context ? `${formatTokens(metadata?.limit?.context)} ctx` : ''}
+ {metadata?.limit?.context && metadata?.limit?.output ? ' • ' : ''}
+ {metadata?.limit?.output ? `${formatTokens(metadata?.limit?.output)} out` : ''}
+
+ )}
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Recent Section for Mobile */}
+ {!mobileModelQuery && recentModelsList.length > 0 && (
+
+
+
+ Recent
+
+
+ {recentModelsList.map(({ model, providerID, modelID }) => {
+ const isSelected = providerID === currentProviderId && modelID === currentModelId;
+ const metadata = getModelMetadata(providerID, modelID);
+
+ return (
+
handleProviderAndModelChange(providerID, modelID)}
+ className={cn(
+ 'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 text-left last:border-b-0',
+ 'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
+ 'first:rounded-t-xl last:rounded-b-xl transition-colors',
+ isSelected ? 'bg-interactive-selection/15 text-interactive-selection-foreground' : 'hover:bg-interactive-hover'
+ )}
+ >
+
+
+
+ {getModelDisplayName(model)}
+
+
+
+ {(metadata?.limit?.context || metadata?.limit?.output) && (
+
+ {metadata?.limit?.context ? `${formatTokens(metadata?.limit?.context)} ctx` : ''}
+ {metadata?.limit?.context && metadata?.limit?.output ? ' • ' : ''}
+ {metadata?.limit?.output ? `${formatTokens(metadata?.limit?.output)} out` : ''}
+
+ )}
+
+
+ );
+ })}
+
+
+ )}
+
+ {filteredProviders.map(({ provider, providerModels }) => {
+ if (providerModels.length === 0 && !normalizedQuery.length) {
+ return null;
+ }
+
+ const isActiveProvider = provider.id === currentProviderId;
+ const isExpanded = expandedMobileProviders.has(provider.id) || normalizedQuery.length > 0;
+
+ return (
+
+
toggleMobileProviderExpansion(provider.id)}
+ className="flex w-full items-center justify-between gap-1.5 px-2 py-1.5 text-left"
+ aria-expanded={isExpanded}
+ >
+
+
+
+ {provider.name}
+
+ {isActiveProvider && (
+
Current
+ )}
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+ {isExpanded && providerModels.length > 0 && (
+
+ {providerModels.map((model: ProviderModel) => {
+ const isSelected = isActiveProvider && model.id === currentModelId;
+ const metadata = getModelMetadata(provider.id, model.id!);
+ const capabilityIcons = getCapabilityIcons(metadata).slice(0, 3);
+ const inputIcons = getModalityIcons(metadata, 'input');
+
+ return (
+
+
handleProviderAndModelChange(provider.id as string, model.id as string)}
+ className={cn(
+ 'flex flex-1 min-w-0 items-start gap-2 text-left',
+ 'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary'
+ )}
+ >
+
+
+ {getModelDisplayName(model)}
+
+
+
+ {(metadata?.limit?.context || metadata?.limit?.output) && (
+
+ {metadata?.limit?.context ? {formatTokens(metadata?.limit?.context)} ctx : null}
+ {metadata?.limit?.context && metadata?.limit?.output ? • : null}
+ {metadata?.limit?.output ? {formatTokens(metadata?.limit?.output)} out : null}
+
+ )}
+ {(capabilityIcons.length > 0 || inputIcons.length > 0) && (
+
+ {[...capabilityIcons, ...inputIcons].map(({ key, icon: IconComponent, label }) => (
+
+
+
+ ))}
+
+ )}
+
+
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ toggleFavoriteModel(provider.id as string, model.id as string);
+ }}
+ className={cn(
+ "model-favorite-button flex h-5 w-5 items-center justify-center hover:text-primary/80 flex-shrink-0",
+ isFavoriteModel(provider.id as string, model.id as string)
+ ? "text-primary"
+ : "text-muted-foreground"
+ )}
+ aria-label={isFavoriteModel(provider.id as string, model.id as string) ? "Unfavorite" : "Favorite"}
+ title={isFavoriteModel(provider.id as string, model.id as string) ? "Remove from favorites" : "Add to favorites"}
+ >
+ {isFavoriteModel(provider.id as string, model.id as string) ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
+
+
+ );
+ };
+
+ const renderMobileVariantPanel = () => {
+ if (!isCompact || !hasVariants) return null;
+
+ const isDefault = !currentVariant;
+
+ const handleSelect = (variant: string | undefined) => {
+ handleVariantSelect(variant);
+ closeMobilePanel();
+ if (onMobilePanelSelection) {
+ requestAnimationFrame(() => {
+ onMobilePanelSelection();
+ });
+ return;
+ }
+ requestAnimationFrame(() => {
+ const textarea = document.querySelector('textarea[data-chat-input="true"]');
+ textarea?.focus();
+ });
+ };
+
+ return (
+
+
+ handleSelect(undefined)}
+ >
+ Default
+ {isDefault && }
+
+
+ {availableVariants.map((variant) => {
+ const selected = currentVariant === variant;
+ const label = variant.charAt(0).toUpperCase() + variant.slice(1);
+
+ return (
+ handleSelect(variant)}
+ >
+ {label}
+ {selected && }
+
+ );
+ })}
+
+
+ );
+ };
+
+ const renderMobileAgentPanel = () => {
+ if (!isCompact) return null;
+
+ return (
+
+
+ Auto-approve edits
+
+
+
+ )}
+ >
+
+ {selectableDesktopAgents.map((agent) => {
+ const isSelected = agent.name === uiAgentName;
+ const agentColor = getAgentColor(agent.name);
+ return (
+
handleAgentChange(agent.name)}
+ >
+
+
+
+ {capitalizeAgentName(agent.name)}
+
+ {isSelected && (
+
+ )}
+
+ {agent.description && (
+
+ {agent.description}
+
+ )}
+
+ );
+ })}
+
+
+ );
+ };
+
+ const renderModelTooltipContent = () => (
+
+ {currentMetadata ? (
+
+
+
+ {currentMetadata.name || getCurrentModelDisplayName()}
+
+ {getProviderDisplayName()}
+
+
+
Capabilities
+
+ {currentCapabilityIcons.length > 0 ? (
+ currentCapabilityIcons.map(({ key, icon, label }) =>
+ renderIconBadge(icon, label, `cap-${key}`)
+ )
+ ) : (
+ —
+ )}
+
+
+
+
Modalities
+
+
+
Input
+
+ {inputModalityIcons.length > 0
+ ? inputModalityIcons.map(({ key, icon, label }) =>
+ renderIconBadge(icon, `${label} input`, `input-${key}`)
+ )
+ : — }
+
+
+
+
Output
+
+ {outputModalityIcons.length > 0
+ ? outputModalityIcons.map(({ key, icon, label }) =>
+ renderIconBadge(icon, `${label} output`, `output-${key}`)
+ )
+ : — }
+
+
+
+
+
+
Cost ($/1M tokens)
+ {costRows.map((row) => (
+
+ {row.label}
+ {row.value}
+
+ ))}
+
+
+
Limits
+ {limitRows.map((row) => (
+
+ {row.label}
+ {row.value}
+
+ ))}
+
+
+
Metadata
+
+ Knowledge
+ {formatKnowledge(currentMetadata.knowledge)}
+
+
+ Release
+ {formatDate(currentMetadata.release_date)}
+
+
+
+ ) : (
+ Model metadata unavailable.
+ )}
+
+ );
+
+ // Helper to render a single model row in the flat dropdown
+ const renderModelRow = (
+ model: ProviderModel,
+ providerID: string,
+ modelID: string,
+ keyPrefix: string,
+ flatIndex: number,
+ isHighlighted: boolean
+ ) => {
+ const metadata = getModelMetadata(providerID, modelID);
+ const capabilityIcons = getCapabilityIcons(metadata).map((icon) => ({
+ ...icon,
+ id: `cap-${icon.key}`,
+ }));
+ const modalityIcons = [
+ ...getModalityIcons(metadata, 'input'),
+ ...getModalityIcons(metadata, 'output'),
+ ];
+ const uniqueModalityIcons = Array.from(
+ new Map(modalityIcons.map((icon) => [icon.key, icon])).values()
+ ).map((icon) => ({ ...icon, id: `mod-${icon.key}` }));
+ const indicatorIcons = [...capabilityIcons, ...uniqueModalityIcons];
+ const contextTokens = formatTokens(metadata?.limit?.context);
+ const isSelected = currentProviderId === providerID && currentModelId === modelID;
+ const isFavorite = isFavoriteModel(providerID, modelID);
+
+ const showProviderLogo = keyPrefix === 'fav' || keyPrefix === 'recent';
+
+ // Build animated metadata slides for desktop
+ const priceText = formatCompactPrice(metadata);
+ const hasPrice = priceText !== null;
+ const hasCapabilities = indicatorIcons.length > 0;
+
+ // Build slides array: price first, then capabilities
+ const slides: React.ReactNode[] = [];
+ if (hasPrice) {
+ slides.push(
+
+ {priceText}
+
+ );
+ }
+ if (hasCapabilities) {
+ slides.push(
+
+ {indicatorIcons.map(({ id, icon: Icon, label }) => (
+
+
+
+ ))}
+
+ );
+ }
+
+ // Rotate metadata in interactive desktop-style pickers (web/desktop), keep VS Code static.
+ const supportsRotatingMetadata = !isVSCodeRuntime;
+ const shouldAnimate = supportsRotatingMetadata && slides.length > 1 && (isHighlighted || isSelected);
+ const staticSlideIndex = !supportsRotatingMetadata && hasCapabilities && hasPrice ? 1 : 0;
+ const staticMetadataSlide = slides[staticSlideIndex];
+
+ return (
+ { modelItemRefs.current[flatIndex] = el; }}
+ className={cn(
+ "typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
+ isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50"
+ )}
+ onClick={() => handleProviderAndModelChange(providerID, modelID)}
+ onMouseEnter={() => setModelSelectedIndex(flatIndex)}
+ >
+
+ {showProviderLogo && (
+
+ )}
+
+ {getModelDisplayName(model)}
+
+ {metadata?.limit?.context ? (
+
+ {contextTokens}
+
+ ) : null}
+
+
+ {/* Metadata slot: animated TextLoop for desktop highlighted/selected rows, static otherwise */}
+ {slides.length > 0 && (
+
+ {shouldAnimate ? (
+
+ {slides}
+
+ ) : (
+ <>
+ {/* In static runtimes (VS Code), prefer capabilities over price when both exist. */}
+ {staticMetadataSlide}
+ >
+ )}
+
+ )}
+ {isSelected && (
+
+ )}
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ toggleFavoriteModel(providerID, modelID);
+ }}
+ className={cn(
+ "model-favorite-button flex h-4 w-4 items-center justify-center hover:text-primary/80",
+ isFavorite ? "text-primary" : "text-muted-foreground"
+ )}
+ aria-label={isFavorite ? "Unfavorite" : "Favorite"}
+ title={isFavorite ? "Remove from favorites" : "Add to favorites"}
+ >
+ {isFavorite ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+ };
+
+ // Filter models based on search query
+ const filterByQuery = (modelName: string, providerName: string, query: string) => {
+ if (!query.trim()) return true;
+ return (
+ matchesModelSearch(modelName, query) ||
+ matchesModelSearch(providerName, query)
+ );
+ };
+
+ const renderModelSelector = () => {
+ const normalizedDesktopQuery = desktopModelQuery.trim();
+ const forceExpandProviders = normalizedDesktopQuery.length > 0;
+
+ // Filter favorites
+ const filteredFavorites = favoriteModelsList.filter(({ model, providerID }) => {
+ const provider = providers.find(p => p.id === providerID);
+ const providerName = provider?.name || providerID;
+ const modelName = getModelDisplayName(model);
+ return filterByQuery(modelName, providerName, desktopModelQuery);
+ });
+
+ // Filter recents
+ const filteredRecents = recentModelsList.filter(({ model, providerID }) => {
+ const provider = providers.find(p => p.id === providerID);
+ const providerName = provider?.name || providerID;
+ const modelName = getModelDisplayName(model);
+ return filterByQuery(modelName, providerName, desktopModelQuery);
+ });
+
+ // Filter providers and their models
+ const filteredProviders = visibleProviders
+ .map((provider) => {
+ const providerModels = Array.isArray(provider.models) ? provider.models : [];
+ const filteredModels = providerModels.filter((model: ProviderModel) => {
+ const modelName = getModelDisplayName(model);
+ return filterByQuery(modelName, provider.name || provider.id || '', desktopModelQuery);
+ });
+ return { ...provider, models: filteredModels };
+ })
+ .filter((provider) => provider.models.length > 0);
+
+ const providerSections = filteredProviders.map((provider) => {
+ const providerId = typeof provider.id === 'string' ? provider.id : '';
+ const isExpanded = forceExpandProviders || !collapsedProviderSet.has(providerId);
+ const models = Array.isArray(provider.models) ? (provider.models as ProviderModel[]) : [];
+ return {
+ provider,
+ isExpanded,
+ models,
+ visibleModels: isExpanded ? models : [],
+ };
+ });
+
+ const hasResults =
+ filteredFavorites.length > 0 ||
+ filteredRecents.length > 0 ||
+ filteredProviders.length > 0;
+
+ // Build flat list for keyboard navigation
+ type FlatModelItem = { model: ProviderModel; providerID: string; modelID: string; section: string };
+ const flatModelList: FlatModelItem[] = [];
+
+ filteredFavorites.forEach(({ model, providerID, modelID }) => {
+ flatModelList.push({ model, providerID, modelID, section: 'fav' });
+ });
+ filteredRecents.forEach(({ model, providerID, modelID }) => {
+ flatModelList.push({ model, providerID, modelID, section: 'recent' });
+ });
+ providerSections.forEach(({ provider, visibleModels }) => {
+ visibleModels.forEach((model) => {
+ flatModelList.push({ model, providerID: provider.id as string, modelID: model.id as string, section: 'provider' });
+ });
+ });
+
+ const totalItems = flatModelList.length;
+
+ // Handle keyboard navigation
+ const handleModelKeyDown = (e: React.KeyboardEvent) => {
+ e.stopPropagation();
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setModelSelectedIndex((prev) => (prev + 1) % Math.max(1, totalItems));
+ // Scroll into view
+ setTimeout(() => {
+ const nextIndex = (modelSelectedIndex + 1) % Math.max(1, totalItems);
+ modelItemRefs.current[nextIndex]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }, 0);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setModelSelectedIndex((prev) => (prev - 1 + Math.max(1, totalItems)) % Math.max(1, totalItems));
+ // Scroll into view
+ setTimeout(() => {
+ const prevIndex = (modelSelectedIndex - 1 + Math.max(1, totalItems)) % Math.max(1, totalItems);
+ modelItemRefs.current[prevIndex]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }, 0);
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ const selectedItem = flatModelList[modelSelectedIndex];
+ if (selectedItem) {
+ handleProviderAndModelChange(selectedItem.providerID, selectedItem.modelID);
+ }
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ setAgentMenuOpen(false);
+ }
+ };
+
+ // Build index mapping for rendering
+ let currentFlatIndex = 0;
+
+ return (
+
+ {!isCompact ? (
+
+
+
+
+ {currentProviderId ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
+ {currentModelDisplayName}
+
+
+
+
+
+
+ {/* Search Input */}
+
+
+
+ setDesktopModelQuery(e.target.value)}
+ onKeyDown={handleModelKeyDown}
+ className="pl-8 h-8 typography-meta"
+ autoFocus
+ />
+
+
+
+ {/* Scrollable content */}
+
+
+
{
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ openAddProviderSettings();
+ }
+ }}
+ className="typography-meta group flex items-center gap-1 rounded-md px-2 py-1.5 cursor-pointer hover:bg-interactive-hover/50"
+ >
+
+
+
+ Add new provider
+
+
+
+
+ {!hasResults && (
+
+ No models found
+
+ )}
+
+ {/* Favorites Section */}
+ {filteredFavorites.length > 0 && (
+ <>
+
+
+ Favorites
+
+ {filteredFavorites.map(({ model, providerID, modelID }) => {
+ const idx = currentFlatIndex++;
+ return renderModelRow(model, providerID, modelID, 'fav', idx, modelSelectedIndex === idx);
+ })}
+ >
+ )}
+
+ {/* Recents Section */}
+ {filteredRecents.length > 0 && (
+ <>
+ {filteredFavorites.length > 0 &&
}
+
+
+ Recent
+
+ {filteredRecents.map(({ model, providerID, modelID }) => {
+ const idx = currentFlatIndex++;
+ return renderModelRow(model, providerID, modelID, 'recent', idx, modelSelectedIndex === idx);
+ })}
+ >
+ )}
+
+ {/* Separator before providers */}
+ {(filteredFavorites.length > 0 || filteredRecents.length > 0) && filteredProviders.length > 0 && (
+
+ )}
+
+ {/* All Providers - Flat List */}
+ {providerSections.map(({ provider, isExpanded, visibleModels }, index) => (
+
+ {index > 0 && }
+ {
+ if (forceExpandProviders) {
+ return;
+ }
+ toggleModelProviderCollapsed(String(provider.id));
+ setModelSelectedIndex(0);
+ }}
+ onKeyDown={(event) => {
+ if (forceExpandProviders) {
+ return;
+ }
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ toggleModelProviderCollapsed(String(provider.id));
+ setModelSelectedIndex(0);
+ }
+ }}
+ className={cn(
+ 'typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex w-full items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30',
+ 'bg-[var(--surface-elevated)] text-left transition-colors',
+ forceExpandProviders ? 'cursor-default' : 'cursor-pointer'
+ )}
+ aria-expanded={isExpanded}
+ title={forceExpandProviders ? undefined : (isExpanded ? 'Collapse provider' : 'Expand provider')}
+ >
+
+
+
{provider.name}
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isExpanded && visibleModels.map((model: ProviderModel) => {
+ const idx = currentFlatIndex++;
+ return renderModelRow(model, provider.id as string, model.id as string, 'provider', idx, modelSelectedIndex === idx);
+ })}
+
+ ))}
+
+
+
+ {/* Keyboard hints footer */}
+
+ ↑↓ navigate • Enter select • Esc close
+
+
+
+ ) : (
+ setActiveMobilePanel('model')}
+ onTouchStart={() => handleLongPressStart('model')}
+ onTouchEnd={handleLongPressEnd}
+ onTouchCancel={handleLongPressEnd}
+ className={cn(
+ 'model-controls__model-trigger flex items-center gap-1.5 min-w-0 focus:outline-none',
+ 'cursor-pointer hover:bg-transparent hover:opacity-70',
+ buttonHeight
+ )}
+ >
+ {currentProviderId ? (
+
+ ) : (
+
+ )}
+
+
+ {currentModelDisplayName}
+
+
+
+ )}
+ {renderModelTooltipContent()}
+
+ );
+ };
+
+ const renderAgentTooltipContent = () => {
+ if (!currentAgent) {
+ return (
+
+ No agent selected.
+
+ );
+ }
+
+ const hasCustomPrompt = Boolean(currentAgent.prompt && currentAgent.prompt.trim().length > 0);
+ const hasModelConfig = currentAgent.model?.providerID && currentAgent.model?.modelID;
+ const hasTemperatureOrTopP = currentAgent.temperature !== undefined || currentAgent.topP !== undefined;
+
+ const summarizePermission = (permissionName: string): { mode: EditPermissionMode; label: string } => {
+ const rules = asPermissionRuleset(currentAgent.permission) ?? [];
+ const hasCustom = rules.some((rule) => rule.permission === permissionName && rule.pattern !== '*');
+ const action = resolveWildcardPermissionAction(rules, permissionName) ?? 'ask';
+
+ if (hasCustom) {
+ return { mode: 'ask', label: 'Custom' };
+ }
+
+ if (action === 'allow') return { mode: 'allow', label: 'Allow' };
+ if (action === 'deny') return { mode: 'deny', label: 'Deny' };
+ return { mode: 'ask', label: 'Ask' };
+ };
+
+ const editPermissionSummary = summarizePermission('edit');
+ const bashPermissionSummary = summarizePermission('bash');
+ const webfetchPermissionSummary = summarizePermission('webfetch');
+
+ return (
+
+
+
+
+ {capitalizeAgentName(currentAgent.name)}
+
+ {currentAgent.description && (
+ {currentAgent.description}
+ )}
+
+
+
+ Mode
+
+ {currentAgent.mode === 'primary' ? 'Primary' : currentAgent.mode === 'subagent' ? 'Subagent' : currentAgent.mode === 'all' ? 'All' : '—'}
+
+
+
+ {(hasModelConfig || hasTemperatureOrTopP) && (
+
+
Model
+ {hasModelConfig ? (
+
+ {currentAgent.model!.providerID} / {currentAgent.model!.modelID}
+
+ ) : (
+
—
+ )}
+ {hasTemperatureOrTopP && (
+
+ {currentAgent.temperature !== undefined && (
+
+ Temperature
+ {currentAgent.temperature}
+
+ )}
+ {currentAgent.topP !== undefined && (
+
+ Top P
+ {currentAgent.topP}
+
+ )}
+
+ )}
+
+ )}
+
+
+
+
Permissions
+
+
Edit
+
+ {renderEditModeIcon(editPermissionSummary.mode, 'h-3.5 w-3.5')}
+
+ {editPermissionSummary.label}
+
+
+
+
+
Bash
+
+ {renderEditModeIcon(bashPermissionSummary.mode, 'h-3.5 w-3.5')}
+
+ {bashPermissionSummary.label}
+
+
+
+
+
WebFetch
+
+ {renderEditModeIcon(webfetchPermissionSummary.mode, 'h-3.5 w-3.5')}
+
+ {webfetchPermissionSummary.label}
+
+
+
+
+
+ {hasCustomPrompt && (
+
+ Custom Prompt
+
+
+ )}
+
+
+ );
+ };
+
+ const renderVariantSelector = () => {
+ if (!hasVariants) {
+ return null;
+ }
+
+ const displayVariant = currentVariant ?? 'Default';
+ const isDefault = !currentVariant;
+ const colorClass = isDefault ? 'text-muted-foreground' : 'text-[color:var(--status-info)]';
+
+ if (isCompact) {
+ return (
+ setActiveMobilePanel('variant')}
+ className={cn(
+ 'model-controls__variant-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
+ buttonHeight,
+ 'cursor-pointer hover:bg-transparent hover:opacity-70',
+ )}
+ >
+
+
+ {displayVariant}
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {displayVariant}
+
+
+
+
+
+ Thinking
+ handleVariantSelect(undefined)}>
+
+ Default
+ {isDefault && }
+
+
+ {availableVariants.length > 0 && }
+ {availableVariants.map((variant) => {
+ const selected = currentVariant === variant;
+ const label = variant.charAt(0).toUpperCase() + variant.slice(1);
+ return (
+ handleVariantSelect(variant)}
+ >
+
+ {label}
+ {selected && }
+
+
+ );
+ })}
+
+
+
+ Thinking: {displayVariant}
+
+
+ );
+ };
+
+ const renderAgentSelector = () => {
+ if (!isCompact) {
+ return (
+
+
+
+
+
+
+
+
+ {getAgentDisplayName()}
+
+
+
+
+
+
+
+
+ setAgentSearchQuery(e.target.value)}
+ onKeyDown={(e) => {
+ e.stopPropagation();
+ }}
+ className="pl-8 h-8 typography-meta"
+ autoFocus
+ />
+
+
+
+
+ {!agentSearchQuery.trim() && defaultAgentName && (
+ <>
+
handleAgentChange(defaultAgentName)}
+ >
+
+
+ Reset to default
+
+
+
+ >
+ )}
+ {sortedAndFilteredAgents.length === 0 ? (
+
+ No agents found
+
+ ) : (
+ sortedAndFilteredAgents.map((agent) => (
+
handleAgentChange(agent.name)}
+ >
+
+
+
+
{capitalizeAgentName(agent.name)}
+
+ {agent.description && (
+
+ {agent.description}
+
+ )}
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+ Auto-approve edits
+
+
+
+
+
+
+
+ {renderAgentTooltipContent()}
+
+
+ );
+ }
+
+ return (
+ setActiveMobilePanel('agent')}
+ onTouchStart={() => handleLongPressStart('agent')}
+ onTouchEnd={handleLongPressEnd}
+ onTouchCancel={handleLongPressEnd}
+ className={cn(
+ 'model-controls__agent-trigger flex items-center gap-1.5 transition-colors min-w-0 focus:outline-none',
+ buttonHeight,
+ 'cursor-pointer hover:bg-transparent hover:opacity-70',
+ )}
+ >
+
+
+ {getAgentDisplayName()}
+
+
+ );
+ };
+
+ const inlineClassName = cn(
+ '@container/model-controls flex items-center min-w-0',
+ // Only force full-width + truncation behaviors on true mobile layouts.
+ // VS Code also uses "compact" mode, but should keep its right-aligned inline sizing.
+ isMobile && 'w-full',
+ className,
+ );
+
+ return (
+ <>
+
+
+ {renderVariantSelector()}
+ {renderModelSelector()}
+ {renderAgentSelector()}
+
+
+
+ {renderMobileModelPanel()}
+ {renderMobileVariantPanel()}
+ {renderMobileAgentPanel()}
+ {renderMobileModelTooltip()}
+ {renderMobileAgentTooltip()}
+ >
+ );
+
+};
diff --git a/ui/src/components/chat/PermissionCard.tsx b/ui/src/components/chat/PermissionCard.tsx
new file mode 100644
index 0000000..2fee9c5
--- /dev/null
+++ b/ui/src/components/chat/PermissionCard.tsx
@@ -0,0 +1,468 @@
+import React from 'react';
+import { RiCheckLine, RiCloseLine, RiFileEditLine, RiGlobalLine, RiPencilAiLine, RiQuestionLine, RiTerminalBoxLine, RiTimeLine, RiToolsLine } from '@remixicon/react';
+import { cn } from '@/lib/utils';
+import type { PermissionRequest, PermissionResponse } from '@/types/permission';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { useThemeSystem } from '@/contexts/useThemeSystem';
+import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
+import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
+import { DiffPreview, WritePreview } from './DiffPreview';
+
+interface PermissionCardProps {
+ permission: PermissionRequest;
+ onResponse?: (response: 'once' | 'always' | 'reject') => void;
+}
+
+const getToolIcon = (toolName: string) => {
+ const iconClass = "h-3 w-3";
+ const tool = toolName.toLowerCase();
+
+ if (tool === 'edit' || tool === 'multiedit' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
+ return ;
+ }
+
+ if (tool === 'write' || tool === 'create' || tool === 'file_write') {
+ return ;
+ }
+
+ if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal' || tool === 'shell_command') {
+ return ;
+ }
+
+ if (tool === 'webfetch' || tool === 'fetch' || tool === 'curl' || tool === 'wget') {
+ return ;
+ }
+
+ return ;
+};
+
+const getToolDisplayName = (toolName: string): string => {
+ const tool = toolName.toLowerCase();
+
+ if (tool === 'edit' || tool === 'multiedit' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
+ return 'edit';
+ }
+ if (tool === 'write' || tool === 'create' || tool === 'file_write') {
+ return 'write';
+ }
+ if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal' || tool === 'shell_command') {
+ return 'bash';
+ }
+ if (tool === 'webfetch' || tool === 'fetch' || tool === 'curl' || tool === 'wget') {
+ return 'webfetch';
+ }
+
+ return toolName;
+};
+
+export const PermissionCard: React.FC = ({
+ permission,
+ onResponse
+}) => {
+ const [isResponding, setIsResponding] = React.useState(false);
+ const [hasResponded, setHasResponded] = React.useState(false);
+ const { respondToPermission } = useSessionStore();
+ const isFromSubagent = useSessionStore(
+ React.useCallback((state) => {
+ const currentSessionId = state.currentSessionId;
+ if (!currentSessionId || permission.sessionID === currentSessionId) return false;
+ const sourceSession = state.sessions.find((session) => session.id === permission.sessionID);
+ return Boolean(sourceSession?.parentID && sourceSession.parentID === currentSessionId);
+ }, [permission.sessionID])
+ );
+ const { currentTheme } = useThemeSystem();
+ const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]);
+
+ const handleResponse = async (response: PermissionResponse) => {
+ setIsResponding(true);
+
+ try {
+ await respondToPermission(permission.sessionID, permission.id, response);
+ setHasResponded(true);
+ onResponse?.(response);
+ } catch { /* ignored */ } finally {
+ setIsResponding(false);
+ }
+ };
+
+ if (hasResponded) {
+ return null;
+ }
+
+ const toolName = permission.permission || 'unknown';
+ const tool = toolName.toLowerCase();
+
+ const getMeta = (key: string, fallback: string = ''): string => {
+ const val = permission.metadata[key];
+ return typeof val === 'string' ? val : (typeof val === 'number' ? String(val) : fallback);
+ };
+ const getMetaNum = (key: string): number | undefined => {
+ const val = permission.metadata[key];
+ return typeof val === 'number' ? val : undefined;
+ };
+ const getMetaBool = (key: string): boolean => {
+ const val = permission.metadata[key];
+ return Boolean(val);
+ };
+ const displayToolName = getToolDisplayName(toolName);
+
+ const renderToolContent = () => {
+
+ if (tool === 'bash' || tool === 'shell' || tool === 'shell_command') {
+ const command = getMeta('command') || getMeta('cmd') || getMeta('script');
+ const description = getMeta('description');
+ const workingDir = getMeta('cwd') || getMeta('working_directory') || getMeta('directory') || getMeta('path');
+ const timeout = getMetaNum('timeout');
+
+ return (
+ <>
+ {description && (
+ {description}
+ )}
+ {workingDir && (
+
+ Working Directory: {workingDir}
+
+ )}
+ {timeout && (
+
+ Timeout: {timeout}ms
+
+ )}
+ {}
+ {command && (
+
+
+ {command}
+
+
+ )}
+ >
+ );
+ }
+
+ if (tool === 'edit' || tool === 'multiedit' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
+ const filePath = getMeta('path') || getMeta('file_path') || getMeta('filename') || getMeta('filePath');
+ const changes = getMeta('changes') || getMeta('diff');
+ const replaceAll = getMetaBool('replace_all') || getMetaBool('replaceAll');
+
+ return (
+ <>
+ {replaceAll && (
+
+ ⚠️ Replace All Occurrences
+
+ )}
+ {changes && (
+
+
+
+ )}
+ >
+ );
+ }
+
+ if (tool === 'write' || tool === 'create' || tool === 'file_write') {
+ const filePath = getMeta('path') || getMeta('file_path') || getMeta('filename') || getMeta('filePath');
+ const content = getMeta('content') || getMeta('text') || getMeta('data');
+
+ if (content) {
+ return (
+
+
+
+ );
+ }
+
+ return null;
+ }
+
+ if (tool === 'webfetch' || tool === 'fetch' || tool === 'curl' || tool === 'wget') {
+ const url = getMeta('url') || getMeta('uri') || getMeta('endpoint');
+ const method = getMeta('method') || 'GET';
+ const headers = permission.metadata.headers && typeof permission.metadata.headers === 'object' ? (permission.metadata.headers as Record) : undefined;
+ const body = getMeta('body') || getMeta('data') || getMeta('payload');
+ const timeout = getMetaNum('timeout');
+ const format = getMeta('format') || getMeta('responseType');
+
+ return (
+ <>
+ {url && (
+
+
Request:
+
+
+ {method}
+
+
+ {url}
+
+
+
+ )}
+ {headers && Object.keys(headers).length > 0 && (
+
+
Headers:
+
+
+ {JSON.stringify(headers, null, 2)}
+
+
+
+ )}
+ {body && (
+
+
Body:
+
+
+ {typeof body === 'object' ? JSON.stringify(body, null, 2) : String(body)}
+
+
+
+ )}
+ {(timeout || format) && (
+
+ {timeout && Timeout: {timeout}ms }
+ {timeout && format && • }
+ {format && Response format: {format} }
+
+ )}
+ >
+ );
+ }
+
+ const genericContent = getMeta('command') || getMeta('content') || getMeta('action') || getMeta('operation');
+ const description = getMeta('description');
+
+ return (
+ <>
+ {description && (
+ {description}
+ )}
+ {genericContent && (
+
+
Action:
+
+
+ {String(genericContent)}
+
+
+
+ )}
+ {}
+ {Object.keys(permission.metadata).length > 0 && !genericContent && !description && (
+
+
Details:
+
+
+ {JSON.stringify(permission.metadata, null, 2)}
+
+
+
+ )}
+ >
+ );
+ };
+
+ return (
+
+
+
+ {}
+
+
+
+
+
+ Permission Required
+
+ {isFromSubagent ? (
+
+ From subagent
+
+ ) : null}
+
+
+ {getToolIcon(toolName)}
+ {displayToolName}
+
+
+
+
+ {}
+
+ {permission.patterns.length > 0 && (
+
+
Patterns:
+
+ {permission.patterns.join(", ")}
+
+
+ )}
+
+ {renderToolContent()}
+
+
+ {}
+
+
handleResponse('once')}
+ disabled={isResponding}
+ className={cn(
+ "flex items-center gap-1.5 sm:gap-1 px-3 sm:px-2 py-1.5 sm:py-1 typography-meta font-medium rounded transition-all min-h-[32px] sm:min-h-0 w-full sm:w-auto",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ backgroundColor: 'rgb(var(--status-success) / 0.1)',
+ color: 'var(--status-success)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--status-success) / 0.2)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--status-success) / 0.1)';
+ }}
+ >
+
+ Allow Once
+
+
+ {permission.always.length > 0 ? (
+
handleResponse('always')}
+ disabled={isResponding}
+ className={cn(
+ "flex items-center gap-1.5 sm:gap-1 px-3 sm:px-2 py-1.5 sm:py-1 typography-meta font-medium rounded transition-all min-h-[32px] sm:min-h-0 w-full sm:w-auto",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ backgroundColor: 'rgb(var(--muted) / 0.5)',
+ color: 'var(--muted-foreground)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.7)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.5)';
+ }}
+ >
+
+ {(() => {
+ const always = (permission.always as string[]) || (permission.metadata.always as string[]) || [];
+ if (always.length === 0) return "Always Allow";
+ const displayPatterns = always.slice(0, 2);
+ const text = displayPatterns.join(", ");
+ const hasMore = always.length > 2;
+ return (
+
+ {hasMore ? `Always: ${text}...` : `Always: ${text}`}
+
+ );
+ })()}
+
+ ) : (
+
handleResponse('always')}
+ disabled={isResponding}
+ className={cn(
+ "flex items-center gap-1.5 sm:gap-1 px-3 sm:px-2 py-1.5 sm:py-1 typography-meta font-medium rounded transition-all min-h-[32px] sm:min-h-0 w-full sm:w-auto",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ backgroundColor: 'rgb(var(--muted) / 0.5)',
+ color: 'var(--muted-foreground)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.7)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.5)';
+ }}
+ >
+
+ Always Allow
+
+ )}
+
+
handleResponse('reject')}
+ disabled={isResponding}
+ className={cn(
+ "flex items-center gap-1.5 sm:gap-1 px-3 sm:px-2 py-1.5 sm:py-1 typography-meta font-medium rounded transition-all min-h-[32px] sm:min-h-0 w-full sm:w-auto",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ backgroundColor: 'rgb(var(--status-error) / 0.1)',
+ color: 'var(--status-error)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--status-error) / 0.2)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--status-error) / 0.1)';
+ }}
+ >
+
+ Deny
+
+
+ {isResponding && (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/ui/src/components/chat/PermissionRequest.tsx b/ui/src/components/chat/PermissionRequest.tsx
new file mode 100644
index 0000000..ab4fad3
--- /dev/null
+++ b/ui/src/components/chat/PermissionRequest.tsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import { RiCheckLine, RiCloseLine, RiTimeLine } from '@remixicon/react';
+import { cn } from '@/lib/utils';
+import type { PermissionRequest as PermissionRequestPayload, PermissionResponse } from '@/types/permission';
+import { useSessionStore } from '@/stores/useSessionStore';
+
+interface PermissionRequestProps {
+ permission: PermissionRequestPayload;
+ onResponse?: (response: 'once' | 'always' | 'reject') => void;
+}
+
+export const PermissionRequest: React.FC = ({
+ permission,
+ onResponse
+}) => {
+ const [isResponding, setIsResponding] = React.useState(false);
+ const [hasResponded, setHasResponded] = React.useState(false);
+ const { respondToPermission } = useSessionStore();
+
+ const handleResponse = async (response: PermissionResponse) => {
+ setIsResponding(true);
+
+ try {
+ await respondToPermission(permission.sessionID, permission.id, response);
+ setHasResponded(true);
+ onResponse?.(response);
+ } catch { /* ignored */ } finally {
+ setIsResponding(false);
+ }
+ };
+
+ if (hasResponded) {
+ return null;
+ }
+
+ const command = typeof permission.metadata.command === 'string'
+ ? permission.metadata.command
+ : (permission.patterns?.[0] ?? permission.permission);
+
+ return (
+
+
+
+
+ Permission required:
+
+
+ {command}
+
+
+
+
+
+
handleResponse('once')}
+ disabled={isResponding}
+ className={cn(
+ "flex items-center gap-1 px-2 py-1 typography-meta font-medium rounded border h-6",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ borderColor: 'var(--status-success)',
+ color: 'var(--status-success)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'var(--status-success-background)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'transparent';
+ }}
+ >
+
+ Once
+
+
+
handleResponse('always')}
+ disabled={isResponding}
+ className={cn(
+ "flex items-center gap-1 px-2 py-1 typography-meta font-medium rounded border h-6",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ borderColor: 'var(--status-info)',
+ color: 'var(--status-info)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'var(--status-info-background)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'transparent';
+ }}
+ >
+
+ Always
+
+
+
handleResponse('reject')}
+ disabled={isResponding}
+ className={cn(
+ "flex items-center gap-1 px-2 py-1 typography-meta font-medium rounded border h-6",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ borderColor: 'var(--status-error)',
+ color: 'var(--status-error)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'var(--status-error-background)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'transparent';
+ }}
+ >
+
+ Reject
+
+
+ {isResponding && (
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/ui/src/components/chat/PermissionToastActions.tsx b/ui/src/components/chat/PermissionToastActions.tsx
new file mode 100644
index 0000000..af61d17
--- /dev/null
+++ b/ui/src/components/chat/PermissionToastActions.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+
+interface PermissionToastActionsProps {
+ sessionTitle: string;
+ permissionBody: string;
+ disabled?: boolean;
+ onOnce: () => Promise | void;
+ onAlways: () => Promise | void;
+ onDeny: () => Promise | void;
+}
+
+const truncateToastText = (value: string, maxLength: number): string => {
+ const normalized = value.trim();
+ if (normalized.length <= maxLength) {
+ return normalized;
+ }
+
+ return `${normalized.slice(0, Math.max(0, maxLength - 3))}...`;
+};
+
+export const PermissionToastActions: React.FC = ({
+ sessionTitle,
+ permissionBody,
+ disabled = false,
+ onOnce,
+ onAlways,
+ onDeny,
+}) => {
+ const [isBusy, setIsBusy] = React.useState(false);
+ const actionContext = sessionTitle.trim().length > 0 ? ` for ${sessionTitle}` : '';
+ const sessionPreview = truncateToastText(sessionTitle, 64) || 'Session';
+ const permissionPreview = truncateToastText(permissionBody, 120) || 'Permission details unavailable';
+
+ const handleAction = async (action: () => Promise | void) => {
+ if (isBusy || disabled) return;
+ setIsBusy(true);
+ try {
+ await action();
+ } finally {
+ setIsBusy(false);
+ }
+ };
+
+ return (
+
+
+
+ Session:{' '}
+
+ {sessionPreview}
+
+
+
+ Permission:{' '}
+
+ {permissionPreview}
+
+
+
+
+
+ handleAction(onOnce)}
+ disabled={disabled || isBusy}
+ aria-label={`Approve once${actionContext}`}
+ className={cn(
+ "px-2 py-1 typography-meta font-medium rounded transition-colors h-6",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ backgroundColor: 'rgb(var(--status-success) / 0.1)',
+ color: 'var(--status-success)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--status-success) / 0.2)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--status-success) / 0.1)';
+ }}
+ >
+ Once
+
+
+ handleAction(onAlways)}
+ disabled={disabled || isBusy}
+ aria-label={`Approve always${actionContext}`}
+ className={cn(
+ "px-2 py-1 typography-meta font-medium rounded transition-colors h-6",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ backgroundColor: 'rgb(var(--muted) / 0.5)',
+ color: 'var(--muted-foreground)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.7)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--muted) / 0.5)';
+ }}
+ >
+ Always
+
+
+ handleAction(onDeny)}
+ disabled={disabled || isBusy}
+ aria-label={`Deny permission${actionContext}`}
+ className={cn(
+ "px-2 py-1 typography-meta font-medium rounded transition-colors h-6",
+ "disabled:opacity-50 disabled:cursor-not-allowed"
+ )}
+ style={{
+ backgroundColor: 'rgb(var(--status-error) / 0.1)',
+ color: 'var(--status-error)'
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--status-error) / 0.2)';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'rgb(var(--status-error) / 0.1)';
+ }}
+ >
+ Deny
+
+
+
+ );
+};
diff --git a/ui/src/components/chat/QuestionCard.tsx b/ui/src/components/chat/QuestionCard.tsx
new file mode 100644
index 0000000..fa6a670
--- /dev/null
+++ b/ui/src/components/chat/QuestionCard.tsx
@@ -0,0 +1,425 @@
+import React from 'react';
+import { RiArrowRightSLine, RiCheckLine, RiCloseLine, RiEditLine, RiListCheck3, RiQuestionLine } from '@remixicon/react';
+import { Checkbox } from '@/components/ui/checkbox';
+
+import { cn } from '@/lib/utils';
+import type { QuestionRequest } from '@/types/question';
+import { useSessionStore } from '@/stores/useSessionStore';
+
+interface QuestionCardProps {
+ question: QuestionRequest;
+}
+
+type TabKey = string;
+const SUMMARY_TAB = 'summary';
+
+export const QuestionCard: React.FC = ({ question }) => {
+ const { respondToQuestion, rejectQuestion } = useSessionStore();
+ const isFromSubagent = useSessionStore(
+ React.useCallback((state) => {
+ const currentSessionId = state.currentSessionId;
+ if (!currentSessionId || question.sessionID === currentSessionId) return false;
+ const sourceSession = state.sessions.find((session) => session.id === question.sessionID);
+ return Boolean(sourceSession?.parentID && sourceSession.parentID === currentSessionId);
+ }, [question.sessionID])
+ );
+ const [activeTab, setActiveTab] = React.useState('0');
+ const [isResponding, setIsResponding] = React.useState(false);
+ const [hasResponded, setHasResponded] = React.useState(false);
+
+ const [selectedOptions, setSelectedOptions] = React.useState>({});
+ const [customMode, setCustomMode] = React.useState>({});
+ const [customText, setCustomText] = React.useState>({});
+
+ const questions = React.useMemo(() => question.questions ?? [], [question.questions]);
+ const isSummaryTab = activeTab === SUMMARY_TAB;
+ const activeIndex = isSummaryTab ? -1 : Math.max(0, Math.min(questions.length - 1, Number(activeTab) || 0));
+ const activeQuestion = isSummaryTab ? null : questions[activeIndex];
+ const activeHeader = React.useMemo(() => {
+ if (isSummaryTab) return null;
+ const header = activeQuestion?.header?.trim();
+ return header && header.length > 0 ? header : null;
+ }, [activeQuestion?.header, isSummaryTab]);
+
+ React.useEffect(() => {
+ setActiveTab('0');
+ setSelectedOptions({});
+ setCustomMode({});
+ setCustomText({});
+ setHasResponded(false);
+ }, [question.id]);
+
+ const tabs = React.useMemo(() => {
+ const questionTabs = questions.map((q, index) => ({
+ value: String(index),
+ label: q.header?.trim() || `Q${index + 1}`,
+ }));
+ // Add summary tab when multiple questions
+ if (questions.length > 1) {
+ questionTabs.push({ value: SUMMARY_TAB, label: 'Summary' });
+ }
+ return questionTabs;
+ }, [questions]);
+
+ // Helper to get answer display for a question index
+ const getAnswerDisplay = React.useCallback((index: number): string => {
+ const isCustom = Boolean(customMode[index]);
+ if (isCustom) {
+ const value = (customText[index] ?? '').trim();
+ return value || '(no answer)';
+ }
+ const answers = selectedOptions[index] ?? [];
+ return answers.length > 0 ? answers.join(', ') : '(no answer)';
+ }, [customMode, customText, selectedOptions]);
+
+ const isMultiple = Boolean(activeQuestion?.multiple);
+ const selectedForActive = selectedOptions[activeIndex] ?? [];
+ const isCustomActive = Boolean(customMode[activeIndex]);
+
+ const unansweredIndexes = React.useMemo(() => {
+ const pending: number[] = [];
+ for (let index = 0; index < questions.length; index += 1) {
+ const isCustom = Boolean(customMode[index]);
+ if (isCustom) {
+ const value = (customText[index] ?? '').trim();
+ if (!value) pending.push(index);
+ continue;
+ }
+
+ const answers = selectedOptions[index] ?? [];
+ if (answers.length === 0) {
+ pending.push(index);
+ }
+ }
+ return pending;
+ }, [customMode, customText, questions.length, selectedOptions]);
+
+ const requiredSatisfied = React.useMemo(() => {
+ if (questions.length === 0) return false;
+ return unansweredIndexes.length === 0;
+ }, [questions.length, unansweredIndexes.length]);
+
+ const handleNextUnanswered = React.useCallback(() => {
+ if (questions.length === 0 || unansweredIndexes.length === 0) return;
+
+ const start = isSummaryTab ? -1 : activeIndex;
+ for (let offset = 1; offset <= questions.length; offset += 1) {
+ const candidate = (start + offset + questions.length) % questions.length;
+ if (unansweredIndexes.includes(candidate)) {
+ setActiveTab(String(candidate));
+ return;
+ }
+ }
+
+ setActiveTab(String(unansweredIndexes[0]));
+ }, [activeIndex, isSummaryTab, questions.length, unansweredIndexes]);
+
+ const buildAnswersPayload = React.useCallback((): string[][] => {
+ const answers: string[][] = [];
+
+ for (let index = 0; index < questions.length; index += 1) {
+ const isCustom = Boolean(customMode[index]);
+ if (isCustom) {
+ const value = (customText[index] ?? '').trim();
+ answers.push(value ? [value] : []);
+ continue;
+ }
+
+ answers.push(selectedOptions[index] ?? []);
+ }
+
+ return answers;
+ }, [customMode, customText, questions.length, selectedOptions]);
+
+ const handleToggleOption = React.useCallback(
+ (label: string) => {
+ if (!activeQuestion) return;
+
+ setCustomMode((prev) => ({ ...prev, [activeIndex]: false }));
+
+ setSelectedOptions((prev) => {
+ const current = prev[activeIndex] ?? [];
+ if (isMultiple) {
+ const exists = current.includes(label);
+ const next = exists ? current.filter((item) => item !== label) : [...current, label];
+ return { ...prev, [activeIndex]: next };
+ }
+ return { ...prev, [activeIndex]: [label] };
+ });
+ },
+ [activeIndex, activeQuestion, isMultiple]
+ );
+
+ const handleSelectCustom = React.useCallback(() => {
+ setCustomMode((prev) => ({ ...prev, [activeIndex]: true }));
+ setSelectedOptions((prev) => ({ ...prev, [activeIndex]: [] }));
+ }, [activeIndex]);
+
+ const handleConfirm = React.useCallback(async () => {
+ if (!requiredSatisfied) return;
+
+ setIsResponding(true);
+ try {
+ const answers = buildAnswersPayload();
+ await respondToQuestion(question.sessionID, question.id, answers);
+ setHasResponded(true);
+ } catch {
+ // ignored
+ } finally {
+ setIsResponding(false);
+ }
+ }, [buildAnswersPayload, question.id, question.sessionID, requiredSatisfied, respondToQuestion]);
+
+ const handleDismiss = React.useCallback(async () => {
+ setIsResponding(true);
+ try {
+ await rejectQuestion(question.sessionID, question.id);
+ setHasResponded(true);
+ } catch {
+ // ignored
+ } finally {
+ setIsResponding(false);
+ }
+ }, [question.id, question.sessionID, rejectQuestion]);
+
+ if (hasResponded || questions.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+ Input needed
+ {isFromSubagent ? (
+
+ From subagent
+
+ ) : null}
+ {activeHeader ? (
+
+ {activeHeader}
+
+ ) : null}
+
+
+
+
+ {/* Minimal inline tabs for multiple questions */}
+ {tabs.length > 1 ? (
+
+ {tabs.map((tab) => {
+ const isActive = activeTab === tab.value;
+ const isSummary = tab.value === SUMMARY_TAB;
+ const tabIndex = isSummary ? -1 : Number(tab.value);
+ const isAnswered = !isSummary && Number.isFinite(tabIndex) && !unansweredIndexes.includes(tabIndex);
+ return (
+ setActiveTab(tab.value)}
+ className={cn(
+ 'px-2 py-0.5 typography-meta font-medium rounded transition-colors flex items-center gap-1',
+ isActive
+ ? 'bg-interactive-selection/40 text-foreground'
+ : isSummary
+ ? 'text-muted-foreground hover:text-foreground hover:bg-interactive-hover/20'
+ : isAnswered
+ ? 'text-muted-foreground/60 hover:text-muted-foreground hover:bg-interactive-hover/20'
+ : 'text-foreground/85 hover:text-foreground hover:bg-interactive-hover/20'
+ )}
+ >
+ {isSummary ? : null}
+ {tab.label}
+
+ );
+ })}
+
+ ) : null}
+
+ {/* Summary view */}
+ {isSummaryTab ? (
+
+ {questions.map((q, index) => {
+ const answer = getAnswerDisplay(index);
+ const hasAnswer = answer !== '(no answer)';
+ return (
+
setActiveTab(String(index))}
+ className="w-full text-left rounded px-1.5 py-1 hover:bg-interactive-hover/20 transition-colors"
+ >
+ {q.header || `Question ${index + 1}`}
+
+ {answer}
+
+
+ );
+ })}
+
+ ) : activeQuestion ? (
+ <>
+
{activeQuestion.question}
+
+ {isMultiple ? (
+
Select multiple
+ ) : null}
+
+
+ {activeQuestion.options.map((option, index) => {
+ const selected = selectedForActive.includes(option.label);
+ const recommended = /\(recommended\)/i.test(option.label);
+
+ return (
+
handleToggleOption(option.label)}
+ disabled={isResponding}
+ className={cn(
+ 'w-full px-1.5 py-1 text-left rounded transition-colors',
+ 'hover:bg-interactive-hover/30',
+ selected ? 'bg-interactive-selection/20' : null,
+ isResponding ? 'opacity-60 cursor-not-allowed' : null
+ )}
+ >
+
+
+ handleToggleOption(option.label)}
+ disabled={isResponding}
+ />
+
+
+
+
+
+ {option.label}
+
+ {recommended ? (
+ recommended
+ ) : null}
+
+ {option.description ? (
+
{option.description}
+ ) : null}
+
+
+
+ );
+ })}
+
+ {/* Custom answer option */}
+
+
+
+
+ Other…
+
+
+
+
+ {isCustomActive ? (
+
+ {
+ if (el) {
+ el.style.height = 'auto';
+ const lineHeight = 20; // approx typography-meta line height
+ const minHeight = lineHeight * 2;
+ const maxHeight = lineHeight * 4;
+ el.style.height = `${Math.min(Math.max(el.scrollHeight, minHeight), maxHeight)}px`;
+ }
+ }}
+ value={customText[activeIndex] ?? ''}
+ onChange={(event: React.ChangeEvent) => {
+ const el = event.target;
+ el.style.height = 'auto';
+ const lineHeight = 20;
+ const minHeight = lineHeight * 2;
+ const maxHeight = lineHeight * 4;
+ el.style.height = `${Math.min(Math.max(el.scrollHeight, minHeight), maxHeight)}px`;
+ setCustomText((prev) => ({ ...prev, [activeIndex]: el.value }));
+ }}
+ placeholder="Your answer"
+ disabled={isResponding}
+ rows={2}
+ className="w-full bg-transparent border border-border/30 focus:border-primary rounded px-2 py-1 outline-none typography-meta text-foreground placeholder:text-muted-foreground/50 transition-colors resize-none overflow-hidden"
+ autoFocus
+ />
+
+ ) : null}
+
+ >
+ ) : null}
+
+
+ {/* Footer actions */}
+
+
+ {requiredSatisfied ? : }
+ {requiredSatisfied ? 'Submit' : 'Next'}
+
+
+
+
+ Dismiss
+
+
+ {isResponding ? (
+
+ ) : null}
+
+
+
+
+ );
+};
diff --git a/ui/src/components/chat/QueuedMessageChips.tsx b/ui/src/components/chat/QueuedMessageChips.tsx
new file mode 100644
index 0000000..b36ca38
--- /dev/null
+++ b/ui/src/components/chat/QueuedMessageChips.tsx
@@ -0,0 +1,116 @@
+import React, { memo } from 'react';
+import { RiCloseLine, RiMessage2Line } from '@remixicon/react';
+import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useFileStore } from '@/stores/fileStore';
+
+interface QueuedMessageChipProps {
+ message: QueuedMessage;
+ sessionId: string;
+ onEdit: (message: QueuedMessage) => void;
+}
+
+const QueuedMessageChip = memo(({ message, sessionId, onEdit }: QueuedMessageChipProps) => {
+ const removeFromQueue = useMessageQueueStore((state) => state.removeFromQueue);
+
+ // Get first line of message, truncated
+ const firstLine = React.useMemo(() => {
+ const lines = message.content.split('\n');
+ const first = lines[0] || '';
+ const maxLength = 100;
+ if (first.length > maxLength) {
+ return first.substring(0, maxLength) + '...';
+ }
+ return first + (lines.length > 1 ? '...' : '');
+ }, [message.content]);
+
+ const attachmentCount = message.attachments?.length ?? 0;
+
+ return (
+ onEdit(message)}
+ className="flex w-full items-center gap-1.5 text-sm hover:opacity-80 transition-opacity text-left h-5 px-1"
+ >
+
+
+ Queued
+ {attachmentCount > 0 && (
+ +{attachmentCount} file{attachmentCount > 1 ? 's' : ''}
+ )}
+
+
+ {firstLine || '(empty)'}
+
+ {
+ e.stopPropagation();
+ removeFromQueue(sessionId, message.id);
+ }}
+ className="flex items-center justify-center h-6 w-6 flex-shrink-0 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer"
+ aria-label="Remove from queue"
+ >
+
+
+
+ );
+});
+
+QueuedMessageChip.displayName = 'QueuedMessageChip';
+
+interface QueuedMessageChipsProps {
+ onEditMessage: (content: string, attachments?: QueuedMessage['attachments']) => void;
+}
+
+const EMPTY_QUEUE: QueuedMessage[] = [];
+
+export const QueuedMessageChips = memo(({ onEditMessage }: QueuedMessageChipsProps) => {
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+ const queuedMessages = useMessageQueueStore(
+ React.useCallback(
+ (state) => {
+ if (!currentSessionId) return EMPTY_QUEUE;
+ return state.queuedMessages[currentSessionId] ?? EMPTY_QUEUE;
+ },
+ [currentSessionId]
+ )
+ );
+ const popToInput = useMessageQueueStore((state) => state.popToInput);
+
+ const handleEdit = React.useCallback((message: QueuedMessage) => {
+ if (!currentSessionId) return;
+
+ const popped = popToInput(currentSessionId, message.id);
+ if (popped) {
+ // Restore attachments to file store if any
+ if (popped.attachments && popped.attachments.length > 0) {
+ const currentAttachments = useFileStore.getState().attachedFiles;
+ useFileStore.setState({
+ attachedFiles: [...currentAttachments, ...popped.attachments]
+ });
+ }
+ onEditMessage(popped.content, popped.attachments);
+ }
+ }, [currentSessionId, popToInput, onEditMessage]);
+
+ if (queuedMessages.length === 0 || !currentSessionId) {
+ return null;
+ }
+
+ return (
+
+ {queuedMessages.map((message) => (
+
+ ))}
+
+ );
+});
+
+QueuedMessageChips.displayName = 'QueuedMessageChips';
diff --git a/ui/src/components/chat/SkillAutocomplete.tsx b/ui/src/components/chat/SkillAutocomplete.tsx
new file mode 100644
index 0000000..f5f0c4e
--- /dev/null
+++ b/ui/src/components/chat/SkillAutocomplete.tsx
@@ -0,0 +1,172 @@
+import React from 'react';
+import { cn, fuzzyMatch } from '@/lib/utils';
+import { useSkillsStore } from '@/stores/useSkillsStore';
+import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
+
+interface SkillInfo {
+ name: string;
+ scope: string;
+ description?: string;
+}
+
+export interface SkillAutocompleteHandle {
+ handleKeyDown: (key: string) => void;
+}
+
+interface SkillAutocompleteProps {
+ searchQuery: string;
+ onSkillSelect: (skillName: string) => void;
+ onClose: () => void;
+ style?: React.CSSProperties;
+}
+
+export const SkillAutocomplete = React.forwardRef(({
+ searchQuery,
+ onSkillSelect,
+ onClose,
+ style,
+}, ref) => {
+ const containerRef = React.useRef(null);
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
+ const [filteredSkills, setFilteredSkills] = React.useState([]);
+ const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
+ const { skills, loadSkills } = useSkillsStore();
+
+ React.useEffect(() => {
+ // Always trigger loadSkills when autocomplete opens to ensure project context is fresh
+ void loadSkills();
+ }, [loadSkills]);
+
+ React.useEffect(() => {
+ const normalizedQuery = searchQuery.trim();
+ const matches = normalizedQuery.length
+ ? skills.filter((skill) => fuzzyMatch(skill.name, normalizedQuery))
+ : skills;
+
+ const sorted = [...matches].sort((a, b) => {
+ // Sort by project scope first, then name
+ if (a.scope === 'project' && b.scope !== 'project') return -1;
+ if (a.scope !== 'project' && b.scope === 'project') return 1;
+ return a.name.localeCompare(b.name);
+ });
+
+ setFilteredSkills(sorted);
+ setSelectedIndex(0);
+ }, [skills, searchQuery]);
+
+ React.useEffect(() => {
+ itemRefs.current[selectedIndex]?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ });
+ }, [selectedIndex]);
+
+ React.useEffect(() => {
+ const handlePointerDown = (event: MouseEvent | TouchEvent) => {
+ const target = event.target as Node | null;
+ if (!target || !containerRef.current) {
+ return;
+ }
+ if (!containerRef.current.contains(target)) {
+ onClose();
+ }
+ };
+
+ document.addEventListener('pointerdown', handlePointerDown, true);
+ return () => {
+ document.removeEventListener('pointerdown', handlePointerDown, true);
+ };
+ }, [onClose]);
+
+ React.useImperativeHandle(ref, () => ({
+ handleKeyDown: (key: string) => {
+ if (key === 'Escape') {
+ onClose();
+ return;
+ }
+
+ if (!filteredSkills.length) {
+ return;
+ }
+
+ if (key === 'ArrowDown') {
+ setSelectedIndex((prev) => (prev + 1) % filteredSkills.length);
+ return;
+ }
+
+ if (key === 'ArrowUp') {
+ setSelectedIndex((prev) => (prev - 1 + filteredSkills.length) % filteredSkills.length);
+ return;
+ }
+
+ if (key === 'Enter' || key === 'Tab') {
+ const skill = filteredSkills[selectedIndex];
+ if (skill) {
+ onSkillSelect(skill.name);
+ }
+ }
+ },
+ }), [filteredSkills, onSkillSelect, onClose, selectedIndex]);
+
+ const renderSkill = (skill: SkillInfo, index: number) => {
+ const isProject = skill.scope === 'project';
+ return (
+ {
+ itemRefs.current[index] = el;
+ }}
+ className={cn(
+ 'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
+ index === selectedIndex && 'bg-interactive-selection'
+ )}
+ onClick={() => onSkillSelect(skill.name)}
+ onMouseEnter={() => setSelectedIndex(index)}
+ >
+
+
+ {skill.name}
+
+ {skill.scope}
+
+
+ {skill.description && (
+
+ {skill.description}
+
+ )}
+
+
+ );
+ };
+
+ return (
+
+
+ {filteredSkills.length ? (
+
+ {filteredSkills.map((skill, index) => renderSkill(skill, index))}
+
+ ) : (
+
+ No skills found
+
+ )}
+
+
+ ↑↓ navigate • Enter select • Esc close
+
+
+ );
+});
+
+SkillAutocomplete.displayName = 'SkillAutocomplete';
diff --git a/ui/src/components/chat/StatusChip.tsx b/ui/src/components/chat/StatusChip.tsx
new file mode 100644
index 0000000..e1d26f2
--- /dev/null
+++ b/ui/src/components/chat/StatusChip.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useContextStore } from '@/stores/contextStore';
+import { formatEffortLabel, getAgentDisplayName, getModelDisplayName } from './mobileControlsUtils';
+
+interface StatusChipProps {
+ onClick: () => void;
+ className?: string;
+}
+
+export const StatusChip: React.FC = ({ onClick, className }) => {
+ const {
+ currentModelId,
+ currentVariant,
+ currentAgentName,
+ getCurrentProvider,
+ getCurrentModelVariants,
+ getVisibleAgents,
+ } = useConfigStore();
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+ const sessionAgentName = useContextStore((state) =>
+ currentSessionId ? state.getSessionAgentSelection(currentSessionId) : null
+ );
+
+ const agents = getVisibleAgents();
+ const uiAgentName = currentSessionId ? (sessionAgentName || currentAgentName) : currentAgentName;
+ const agentLabel = getAgentDisplayName(agents, uiAgentName);
+ const currentProvider = getCurrentProvider();
+ const modelLabel = getModelDisplayName(currentProvider, currentModelId);
+ const hasEffort = getCurrentModelVariants().length > 0;
+ const effortLabel = hasEffort ? formatEffortLabel(currentVariant) : null;
+ const fullLabel = [agentLabel, modelLabel, effortLabel].filter(Boolean).join(' · ');
+
+ return (
+
+ {agentLabel}
+ ·
+ {modelLabel}
+ {effortLabel && (
+ <>
+ ·
+ {effortLabel}
+ >
+ )}
+
+ );
+};
+
+export default StatusChip;
diff --git a/ui/src/components/chat/StatusRow.tsx b/ui/src/components/chat/StatusRow.tsx
new file mode 100644
index 0000000..60d130c
--- /dev/null
+++ b/ui/src/components/chat/StatusRow.tsx
@@ -0,0 +1,294 @@
+import React from "react";
+import {
+ RiArrowDownSLine,
+ RiArrowUpDoubleLine,
+ RiArrowUpSLine,
+ RiCheckboxCircleLine,
+ RiCloseCircleLine,
+ RiRecordCircleLine,
+ RiTimeLine,
+} from "@remixicon/react";
+import { cn } from "@/lib/utils";
+import { useTodoStore, type TodoItem, type TodoPriority, type TodoStatus } from "@/stores/useTodoStore";
+import { useSessionStore } from "@/stores/useSessionStore";
+import { useUIStore } from "@/stores/useUIStore";
+import { WorkingPlaceholder } from "./message/parts/WorkingPlaceholder";
+import { isVSCodeRuntime } from "@/lib/desktop";
+
+const statusConfig: Record = {
+ in_progress: {
+ textClassName: "text-foreground",
+ },
+ pending: {
+ textClassName: "text-foreground",
+ },
+ completed: {
+ textClassName: "text-muted-foreground line-through",
+ },
+ cancelled: {
+ textClassName: "text-muted-foreground line-through",
+ },
+};
+
+const priorityClassName: Record = {
+ high: "text-[var(--status-warning)]",
+ medium: "text-muted-foreground",
+ low: "text-muted-foreground/70",
+};
+
+const priorityIcon: Record = {
+ high: ,
+ medium: ,
+ low: ,
+};
+
+interface TodoItemRowProps {
+ todo: TodoItem;
+}
+
+const TodoItemRow: React.FC = ({ todo }) => {
+ const config = statusConfig[todo.status] || statusConfig.pending;
+
+ const statusIcon =
+ todo.status === "in_progress" ? (
+
+ ) : todo.status === "completed" ? (
+
+ ) : (
+
+ );
+
+ return (
+
+ {statusIcon}
+
+ {todo.content}
+
+
+ {priorityIcon[todo.priority] ?? priorityIcon.medium}
+
+
+ );
+};
+
+const EMPTY_TODOS: TodoItem[] = [];
+
+interface StatusRowProps {
+ // Working state
+ isWorking: boolean;
+ statusText: string | null;
+ isGenericStatus?: boolean;
+ isWaitingForPermission?: boolean;
+ wasAborted?: boolean;
+ abortActive?: boolean;
+ retryInfo?: { attempt?: number; next?: number } | null;
+ // Abort state (for mobile/vscode)
+ showAbort?: boolean;
+ onAbort?: () => void;
+ // Abort status display
+ showAbortStatus?: boolean;
+}
+
+export const StatusRow: React.FC = ({
+ isWorking,
+ statusText,
+ isGenericStatus,
+ isWaitingForPermission,
+ wasAborted,
+ abortActive,
+ retryInfo,
+ showAbort,
+ onAbort,
+ showAbortStatus,
+}) => {
+ const [isExpanded, setIsExpanded] = React.useState(false);
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+ const todos = useTodoStore((state) =>
+ currentSessionId ? state.sessionTodos.get(currentSessionId) ?? EMPTY_TODOS : EMPTY_TODOS
+ );
+ const loadTodos = useTodoStore((state) => state.loadTodos);
+ const { isMobile } = useUIStore();
+ const isCompact = isMobile || isVSCodeRuntime();
+
+ // Load todos when session changes
+ React.useEffect(() => {
+ if (currentSessionId) {
+ void loadTodos(currentSessionId);
+ }
+ }, [currentSessionId, loadTodos]);
+
+ // Filter out cancelled todos for display and keep original order.
+ // This prevents items from jumping around when status changes.
+ const visibleTodos = React.useMemo(() => {
+ return todos.filter((todo) => todo.status !== "cancelled");
+ }, [todos]);
+
+ // Find the current active todo (first in_progress, or first pending)
+ const activeTodo = React.useMemo(() => {
+ return (
+ visibleTodos.find((t) => t.status === "in_progress") ||
+ visibleTodos.find((t) => t.status === "pending") ||
+ null
+ );
+ }, [visibleTodos]);
+
+ // Calculate progress
+ const progress = React.useMemo(() => {
+ const total = todos.filter((t) => t.status !== "cancelled").length;
+ const completed = todos.filter((t) => t.status === "completed").length;
+ return { completed, total };
+ }, [todos]);
+
+ const statusSummary = React.useMemo(() => {
+ const active = visibleTodos.filter((t) => t.status === "in_progress").length;
+ const left = visibleTodos.filter((t) => t.status === "in_progress" || t.status === "pending").length;
+ return { active, left };
+ }, [visibleTodos]);
+
+ const hasActiveTodos = visibleTodos.some((t) => t.status === "in_progress" || t.status === "pending");
+ // Original logic from ChatInput
+ const shouldRenderPlaceholder = !showAbortStatus && (wasAborted || !abortActive);
+
+ // Keep StatusRow rendered while:
+ // - isWorking (active session)
+ // - wasAborted / showAbortStatus
+ // - hasActiveTodos
+ const hasContent =
+ isWorking ||
+ Boolean(wasAborted) ||
+ Boolean(showAbortStatus) ||
+ hasActiveTodos;
+
+ // Close popover when clicking outside
+ const popoverRef = React.useRef(null);
+ React.useEffect(() => {
+ if (!isExpanded) return;
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
+ setIsExpanded(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, [isExpanded]);
+
+ const toggleExpanded = () => setIsExpanded((prev) => !prev);
+
+ // Abort button for mobile/vscode
+ const abortButton = showAbort && onAbort ? (
+
+
+
+ ) : null;
+
+ // Todo trigger button
+ const todoTrigger = hasActiveTodos ? (
+
+ {/* Desktop: show task text; Mobile/VSCode: just "Tasks" */}
+ {!isCompact && activeTodo ? (
+
+ {activeTodo.content}
+
+ ) : (
+ Tasks
+ )}
+
+ {statusSummary.active} active · {statusSummary.left} left
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ ) : null;
+
+ // Don't render if nothing to show
+ if (!hasContent) {
+ return null;
+ }
+
+ return (
+
+
+ {/* Left: Abort status or Working placeholder */}
+
+ {showAbortStatus ? (
+
+
+
+ Aborted
+
+
+ ) : shouldRenderPlaceholder ? (
+
+ ) : null}
+
+
+ {/* Right: Abort (mobile only) + Todo */}
+
+ {abortButton}
+ {todoTrigger}
+
+ {/* Popover dropdown */}
+ {isExpanded && hasActiveTodos && (
+
+ {/* Header */}
+
+ Tasks
+
+ {progress.completed}/{progress.total}
+
+
+
+ {/* Todo list */}
+
+ {visibleTodos.map((todo) => (
+
+ ))}
+
+
+ )}
+
+
+
+ );
+};
diff --git a/ui/src/components/chat/StreamingTextDiff.tsx b/ui/src/components/chat/StreamingTextDiff.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/ui/src/components/chat/TimelineDialog.tsx b/ui/src/components/chat/TimelineDialog.tsx
new file mode 100644
index 0000000..08daddc
--- /dev/null
+++ b/ui/src/components/chat/TimelineDialog.tsx
@@ -0,0 +1,210 @@
+import React from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useMessageStore } from '@/stores/messageStore';
+import { RiLoader4Line, RiSearchLine, RiTimeLine, RiGitBranchLine, RiArrowGoBackLine } from '@remixicon/react';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
+import type { Part } from '@opencode-ai/sdk/v2';
+
+interface TimelineDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onScrollToMessage?: (messageId: string) => void;
+}
+
+// Helper: format relative time (e.g., "2 hours ago")
+function formatRelativeTime(timestamp: number): string {
+ const now = Date.now();
+ const diffMs = now - timestamp;
+ const diffSecs = Math.floor(diffMs / 1000);
+ const diffMins = Math.floor(diffSecs / 60);
+ const diffHours = Math.floor(diffMins / 60);
+ const diffDays = Math.floor(diffHours / 24);
+
+ if (diffSecs < 60) return 'just now';
+ if (diffMins < 60) return `${diffMins}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ if (diffDays < 7) return `${diffDays}d ago`;
+ return new Date(timestamp).toLocaleDateString();
+}
+
+export const TimelineDialog: React.FC = ({ open, onOpenChange, onScrollToMessage }) => {
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+ const messages = useMessageStore((state) =>
+ currentSessionId ? state.messages.get(currentSessionId) || [] : []
+ );
+ const revertToMessage = useSessionStore((state) => state.revertToMessage);
+ const forkFromMessage = useSessionStore((state) => state.forkFromMessage);
+ const loadSessions = useSessionStore((state) => state.loadSessions);
+
+ const [forkingMessageId, setForkingMessageId] = React.useState(null);
+ const [searchQuery, setSearchQuery] = React.useState('');
+
+ // Filter user messages (reversed for newest first)
+ const userMessages = React.useMemo(() => {
+ const filtered = messages.filter(m => m.info.role === 'user');
+ return filtered.reverse();
+ }, [messages]);
+
+ // Filter by search query
+ const filteredMessages = React.useMemo(() => {
+ if (!searchQuery.trim()) return userMessages;
+
+ const query = searchQuery.toLowerCase();
+ return userMessages.filter((message) => {
+ const preview = getMessagePreview(message.parts).toLowerCase();
+ return preview.includes(query);
+ });
+ }, [userMessages, searchQuery]);
+
+ // Handle fork with loading state and session refresh
+ const handleFork = async (messageId: string) => {
+ if (!currentSessionId) return;
+ setForkingMessageId(messageId);
+ try {
+ await forkFromMessage(currentSessionId, messageId);
+ await loadSessions();
+ onOpenChange(false);
+ } finally {
+ setForkingMessageId(null);
+ }
+ };
+
+ if (!currentSessionId) return null;
+
+ return (
+
+
+
+
+
+ Conversation Timeline
+
+
+ Navigate to any point in the conversation or fork a new session
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-9 w-full"
+ />
+
+
+
+ {filteredMessages.length === 0 ? (
+
+ {searchQuery ? 'No messages found' : 'No messages in this session yet'}
+
+ ) : (
+ filteredMessages.map((message) => {
+ const preview = getMessagePreview(message.parts);
+ const timestamp = message.info.time.created;
+ const relativeTime = formatRelativeTime(timestamp);
+ const messageNumber = userMessages.length - userMessages.indexOf(message);
+
+ return (
+
{
+ onScrollToMessage?.(message.info.id);
+ onOpenChange(false);
+ }}
+ >
+
+ {messageNumber}.
+
+
+ {preview || '[No text content]'}
+ {preview && preview.length >= 80 && '…'}
+
+
+
+
+ {relativeTime}
+
+
+
+
+
+ {
+ e.stopPropagation();
+ await revertToMessage(currentSessionId, message.info.id);
+ onOpenChange(false);
+ }}
+ >
+
+
+
+ Revert from here
+
+
+
+
+ {
+ e.stopPropagation();
+ handleFork(message.info.id);
+ }}
+ disabled={forkingMessageId === message.info.id}
+ >
+ {forkingMessageId === message.info.id ? (
+
+ ) : (
+
+ )}
+
+
+ Fork from here
+
+
+
+
+ );
+ })
+ )}
+
+
+
+
Actions
+
+
+ Click on a message to scroll to it in the conversation
+
+
+
+ Undo to this point (message text will populate input)
+
+
+
+ Create a new session starting from here
+
+
+
+
+
+ );
+};
+
+function getMessagePreview(parts: Part[]): string {
+ const textPart = parts.find(p => p.type === 'text');
+ if (!textPart || typeof textPart.text !== 'string') return '';
+ return textPart.text.replace(/\n/g, ' ').slice(0, 80);
+}
diff --git a/ui/src/components/chat/UnifiedControlsDrawer.tsx b/ui/src/components/chat/UnifiedControlsDrawer.tsx
new file mode 100644
index 0000000..1de252c
--- /dev/null
+++ b/ui/src/components/chat/UnifiedControlsDrawer.tsx
@@ -0,0 +1,258 @@
+import React from 'react';
+import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
+import { ProviderLogo } from '@/components/ui/ProviderLogo';
+import { cn } from '@/lib/utils';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useContextStore } from '@/stores/contextStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { useModelLists } from '@/hooks/useModelLists';
+import {
+ formatEffortLabel,
+ getQuickEffortOptions,
+ parseEffortVariant,
+} from './mobileControlsUtils';
+
+const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', {
+ notation: 'compact',
+ maximumFractionDigits: 1,
+});
+
+const formatTokens = (value?: number | null) => {
+ if (typeof value !== 'number' || Number.isNaN(value)) {
+ return null;
+ }
+ if (value === 0) {
+ return '0';
+ }
+ const formatted = COMPACT_NUMBER_FORMATTER.format(value);
+ return formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted;
+};
+
+interface UnifiedControlsDrawerProps {
+ open: boolean;
+ onClose: () => void;
+ onOpenModel: () => void;
+ onOpenEffort: () => void;
+}
+
+export const UnifiedControlsDrawer: React.FC = ({
+ open,
+ onClose,
+ onOpenModel,
+ onOpenEffort,
+}) => {
+ const {
+ providers,
+ currentProviderId,
+ currentModelId,
+ currentVariant,
+ setProvider,
+ setModel,
+ setCurrentVariant,
+ getCurrentModelVariants,
+ getModelMetadata,
+ } = useConfigStore();
+ const { addRecentModel, addRecentEffort, recentEfforts } = useUIStore();
+ const { recentModelsList } = useModelLists();
+ const {
+ currentSessionId,
+ saveAgentModelForSession,
+ saveAgentModelVariantForSession,
+ } = useSessionStore();
+ const sessionAgentName = useContextStore((state) =>
+ currentSessionId ? state.getSessionAgentSelection(currentSessionId) : null
+ );
+
+ const uiAgentName = currentSessionId ? (sessionAgentName || null) : null;
+
+ const recentModelsBase = recentModelsList.slice(0, 4);
+ const hasCurrentInRecents = recentModelsBase.some(
+ (entry) => entry.providerID === currentProviderId && entry.modelID === currentModelId
+ );
+ // If current model not in recents, prepend it so it's always visible
+ const recentModels = React.useMemo(() => {
+ if (hasCurrentInRecents || !currentProviderId || !currentModelId) {
+ return recentModelsBase;
+ }
+ const currentProvider = providers.find((p) => p.id === currentProviderId);
+ const currentModel = currentProvider?.models?.find((m) => m.id === currentModelId);
+ if (!currentModel) {
+ return recentModelsBase;
+ }
+ return [
+ { providerID: currentProviderId, modelID: currentModelId, provider: currentProvider, model: currentModel },
+ ...recentModelsBase.slice(0, 3),
+ ];
+ }, [recentModelsBase, hasCurrentInRecents, currentProviderId, currentModelId, providers]);
+
+ const variants = getCurrentModelVariants();
+ const hasEffort = variants.length > 0;
+ const effortKey = currentProviderId && currentModelId ? `${currentProviderId}/${currentModelId}` : null;
+ const recentEffortsForModel = effortKey ? (recentEfforts[effortKey] ?? []) : [];
+ const recentEffortOptions = recentEffortsForModel
+ .map((variant) => parseEffortVariant(variant))
+ .filter((variant) => !variant || variants.includes(variant));
+ const fallbackEfforts = getQuickEffortOptions(variants);
+ const baseEfforts = fallbackEfforts.length > 0 ? fallbackEfforts : recentEffortOptions;
+ const quickEfforts = React.useMemo(() => {
+ const base = baseEfforts.slice(0, 4);
+ const orderedRecents = recentEffortOptions.slice().reverse();
+ for (const recent of orderedRecents) {
+ if (base.some((entry) => entry === recent)) {
+ continue;
+ }
+ base.unshift(recent);
+ base.splice(4);
+ }
+ if (!base.some((entry) => entry === currentVariant)) {
+ if (base.length > 0) {
+ base[0] = currentVariant;
+ } else {
+ base.push(currentVariant);
+ }
+ }
+ if (!base.some((entry) => entry === undefined)) {
+ base.push(undefined);
+ base.splice(4);
+ }
+ return base;
+ }, [baseEfforts, currentVariant, recentEffortOptions]);
+ const effortHasMore = variants.length + 1 > quickEfforts.length;
+
+ const handleModelSelect = (providerId: string, modelId: string) => {
+ const provider = providers.find((entry) => entry.id === providerId);
+ if (!provider) {
+ return;
+ }
+ const providerModels = Array.isArray(provider.models) ? provider.models : [];
+ const modelExists = providerModels.some((model) => model.id === modelId);
+ if (!modelExists) {
+ return;
+ }
+
+ const isRecentAlready = recentModelsList.some(
+ (entry) => entry.providerID === providerId && entry.modelID === modelId
+ );
+
+ setProvider(providerId);
+ setModel(modelId);
+ if (!isRecentAlready) {
+ addRecentModel(providerId, modelId);
+ }
+
+ if (currentSessionId && uiAgentName) {
+ saveAgentModelForSession(currentSessionId, uiAgentName, providerId, modelId);
+ }
+ };
+
+ const handleEffortSelect = (variant: string | undefined) => {
+ setCurrentVariant(variant);
+ if (currentProviderId && currentModelId) {
+ addRecentEffort(currentProviderId, currentModelId, variant);
+ }
+ if (currentSessionId && uiAgentName && currentProviderId && currentModelId) {
+ saveAgentModelVariantForSession(currentSessionId, uiAgentName, currentProviderId, currentModelId, variant);
+ }
+ };
+
+ return (
+
+
+
+
+ Model
+
+
+ {recentModels.length === 0 && !hasCurrentInRecents && (
+
+ No recent models
+
+ )}
+ {recentModels.map(({ providerID, modelID, model }) => {
+ const isSelected = providerID === currentProviderId && modelID === currentModelId;
+ const modelName = typeof model?.name === 'string' && model.name.trim().length > 0
+ ? model.name
+ : modelID;
+ const metadata = getModelMetadata(providerID, modelID);
+ const ctxTokens = formatTokens(metadata?.limit?.context);
+ const outTokens = formatTokens(metadata?.limit?.output);
+ return (
+
handleModelSelect(providerID, modelID)}
+ className={cn(
+ 'flex min-h-[44px] w-full items-center gap-2 border-b border-border/30 px-3 py-2 text-left last:border-b-0',
+ isSelected ? 'bg-primary/10' : ''
+ )}
+ >
+
+
+ {modelName}
+
+ {(ctxTokens || outTokens) && (
+
+ {ctxTokens && `${ctxTokens} ctx`}
+ {ctxTokens && outTokens && ' • '}
+ {outTokens && `${outTokens} out`}
+
+ )}
+
+ );
+ })}
+
+ ...
+
+
+
+
+ {hasEffort && (
+
+
+ Effort
+
+
+ {quickEfforts.map((variant) => {
+ const isSelected = variant === currentVariant || (!variant && !currentVariant);
+ return (
+ handleEffortSelect(variant)}
+ className={cn(
+ 'inline-flex items-center rounded-full border px-2.5 py-1 typography-meta font-medium',
+ isSelected
+ ? 'border-primary/30 bg-primary/10 text-foreground'
+ : 'border-border/40 text-muted-foreground hover:bg-interactive-hover/50'
+ )}
+ aria-pressed={isSelected}
+ >
+ {formatEffortLabel(variant)}
+
+ );
+ })}
+ {effortHasMore && (
+
+ ...
+
+ )}
+
+
+ )}
+
+
+ );
+};
+
+export default UnifiedControlsDrawer;
diff --git a/ui/src/components/chat/contexts/TurnGroupingContext.tsx b/ui/src/components/chat/contexts/TurnGroupingContext.tsx
new file mode 100644
index 0000000..c9add4f
--- /dev/null
+++ b/ui/src/components/chat/contexts/TurnGroupingContext.tsx
@@ -0,0 +1,656 @@
+/* eslint-disable react-refresh/only-export-components */
+import React from 'react';
+import type { Message, Part } from '@opencode-ai/sdk/v2';
+import type { TurnGroupingContext as TurnGroupingContextType } from '../hooks/useTurnGrouping';
+import { detectTurns, type Turn, type TurnActivityPart, type TurnActivityGroup } from '../hooks/useTurnGrouping';
+import { useCurrentSessionActivity } from '@/hooks/useSessionActivity';
+import { useUIStore } from '@/stores/useUIStore';
+
+interface ChatMessageEntry {
+ info: Message;
+ parts: Part[];
+}
+
+interface TurnDiffStats {
+ additions: number;
+ deletions: number;
+ files: number;
+}
+
+interface TurnActivityInfo {
+ activityParts: TurnActivityPart[];
+ activityGroupSegments: TurnActivityGroup[];
+ hasTools: boolean;
+ hasReasoning: boolean;
+ summaryBody?: string;
+ diffStats?: TurnDiffStats;
+}
+
+interface NeighborInfo {
+ previousMessage?: ChatMessageEntry;
+ nextMessage?: ChatMessageEntry;
+}
+
+// Static data that only changes when messages change
+interface TurnGroupingStaticData {
+ structureKey: string;
+ turns: Turn[];
+ messageToTurn: Map;
+ turnActivityInfo: Map;
+ lastTurnId: string | null;
+ lastTurnMessageIds: Set; // Messages belonging to the last turn
+ defaultActivityExpanded: boolean;
+ // Neighbor lookup - stable until messages change
+ messageNeighbors: Map;
+}
+
+// UI state that changes on user interaction (expand/collapse)
+interface TurnGroupingUiStateData {
+ turnUiStates: Map;
+ toggleGroup: (turnId: string) => void;
+}
+
+// Streaming state that changes frequently during assistant response
+interface TurnGroupingStreamingData {
+ sessionIsWorking: boolean;
+ lastTurnActivityInfo?: TurnActivityInfo;
+}
+
+// Separate contexts to prevent unnecessary re-renders
+const TurnGroupingStaticContext = React.createContext(null);
+const TurnGroupingUiStateContext = React.createContext(null);
+const TurnGroupingStreamingContext = React.createContext(null);
+
+const contextCache = new Map();
+
+export const useTurnGroupingContextForMessage = (messageId: string): TurnGroupingContextType | undefined => {
+ const staticData = React.useContext(TurnGroupingStaticContext);
+ const uiStateData = React.useContext(TurnGroupingUiStateContext);
+ const streamingData = React.useContext(TurnGroupingStreamingContext);
+
+ return React.useMemo(() => {
+ if (!staticData || !uiStateData || !streamingData) return undefined;
+
+ const turn = staticData.messageToTurn.get(messageId);
+ if (!turn) return undefined;
+
+ const isAssistantMessage = turn.assistantMessages.some(
+ (msg) => msg.info.id === messageId
+ );
+ if (!isAssistantMessage) return undefined;
+
+ const isLastTurn = staticData.lastTurnId === turn.turnId;
+ const lastTurnActivityVersion = isLastTurn
+ ? `${streamingData.lastTurnActivityInfo?.activityParts.length ?? 0}:${streamingData.lastTurnActivityInfo?.activityGroupSegments.length ?? 0}:${streamingData.lastTurnActivityInfo?.hasTools ? 1 : 0}:${streamingData.lastTurnActivityInfo?.hasReasoning ? 1 : 0}`
+ : '';
+
+ // Get UI state early - needed for cache key to ensure expand/collapse updates propagate
+ const uiState = uiStateData.turnUiStates.get(turn.turnId) ?? { isExpanded: staticData.defaultActivityExpanded };
+ const isExpanded = uiState.isExpanded;
+
+ // Cache key must include:
+ // - messageId: identifies the specific message
+ // - isExpanded: UI state for this turn's activity group
+ // - sessionIsWorking (last turn only): streaming state affects "working" indicator
+ const cacheKey = isLastTurn
+ ? `${staticData.structureKey}:${messageId}-${isExpanded}-${streamingData.sessionIsWorking}-${lastTurnActivityVersion}`
+ : `${staticData.structureKey}:${messageId}-${isExpanded}`;
+
+ const cached = contextCache.get(cacheKey);
+ if (cached) return cached;
+
+ const activityInfo = isLastTurn
+ ? streamingData.lastTurnActivityInfo
+ : staticData.turnActivityInfo.get(turn.turnId);
+ const activityParts = activityInfo?.activityParts ?? [];
+ const activityGroupSegments = activityInfo?.activityGroupSegments ?? [];
+ const hasTools = Boolean(activityInfo?.hasTools);
+ const hasReasoning = Boolean(activityInfo?.hasReasoning);
+ const summaryBody = activityInfo?.summaryBody;
+ const diffStats = activityInfo?.diffStats;
+
+ const firstAssistantId = turn.assistantMessages[0]?.info.id;
+ const isFirstAssistantInTurn = messageId === firstAssistantId;
+ const lastAssistantId = turn.assistantMessages[turn.assistantMessages.length - 1]?.info.id;
+ const isLastAssistantInTurn = messageId === lastAssistantId;
+ const headerMessageId = firstAssistantId;
+ // Only the last turn can be "working"
+ const isTurnWorking = isLastTurn && streamingData.sessionIsWorking;
+
+ const userTimeInfo = turn.userMessage.info.time as { created?: number } | undefined;
+ const userMessageCreatedAt = typeof userTimeInfo?.created === 'number' ? userTimeInfo.created : undefined;
+
+ const context: TurnGroupingContextType = {
+ turnId: turn.turnId,
+ isFirstAssistantInTurn,
+ isLastAssistantInTurn,
+ summaryBody,
+ activityParts,
+ activityGroupSegments,
+ headerMessageId,
+ hasTools,
+ hasReasoning,
+ diffStats,
+ userMessageCreatedAt,
+ isWorking: isTurnWorking,
+ isGroupExpanded: isExpanded,
+ toggleGroup: () => uiStateData.toggleGroup(turn.turnId),
+ };
+
+ // Cache with size limit
+ if (contextCache.size > 500) {
+ const firstKey = contextCache.keys().next().value;
+ if (firstKey) contextCache.delete(firstKey);
+ }
+ contextCache.set(cacheKey, context);
+
+ return context;
+ }, [staticData, uiStateData, streamingData, messageId]);
+};
+
+// Hook to get neighbor messages - uses context instead of passed messages array
+export const useMessageNeighbors = (messageId: string): NeighborInfo => {
+ const staticData = React.useContext(TurnGroupingStaticContext);
+
+ // Return stable reference from context - no dependencies on messages array
+ return React.useMemo(() => {
+ if (!staticData) return {};
+ return staticData.messageNeighbors.get(messageId) ?? {};
+ }, [staticData, messageId]);
+};
+
+// Hook to get last turn message IDs - only reads static context
+export const useLastTurnMessageIds = (): Set => {
+ const staticData = React.useContext(TurnGroupingStaticContext);
+ return staticData?.lastTurnMessageIds ?? new Set();
+};
+
+// Static-only version of turn grouping context - does NOT subscribe to streaming context
+// Use this for messages NOT in the last turn to avoid re-renders during streaming
+// Still subscribes to UI state context for expand/collapse functionality
+export const useTurnGroupingContextStatic = (messageId: string): TurnGroupingContextType | undefined => {
+ const staticData = React.useContext(TurnGroupingStaticContext);
+ const uiStateData = React.useContext(TurnGroupingUiStateContext);
+
+ return React.useMemo(() => {
+ if (!staticData || !uiStateData) return undefined;
+
+ const turn = staticData.messageToTurn.get(messageId);
+ if (!turn) return undefined;
+
+ const isAssistantMessage = turn.assistantMessages.some(
+ (msg) => msg.info.id === messageId
+ );
+ if (!isAssistantMessage) return undefined;
+
+ const activityInfo = staticData.turnActivityInfo.get(turn.turnId);
+ const activityParts = activityInfo?.activityParts ?? [];
+ const activityGroupSegments = activityInfo?.activityGroupSegments ?? [];
+ const hasTools = Boolean(activityInfo?.hasTools);
+ const hasReasoning = Boolean(activityInfo?.hasReasoning);
+ const summaryBody = activityInfo?.summaryBody;
+ const diffStats = activityInfo?.diffStats;
+
+ const firstAssistantId = turn.assistantMessages[0]?.info.id;
+ const isFirstAssistantInTurn = messageId === firstAssistantId;
+ const lastAssistantId = turn.assistantMessages[turn.assistantMessages.length - 1]?.info.id;
+ const isLastAssistantInTurn = messageId === lastAssistantId;
+ const headerMessageId = firstAssistantId;
+
+ const uiState = uiStateData.turnUiStates.get(turn.turnId) ?? { isExpanded: staticData.defaultActivityExpanded };
+
+ const userTimeInfo = turn.userMessage.info.time as { created?: number } | undefined;
+ const userMessageCreatedAt = typeof userTimeInfo?.created === 'number' ? userTimeInfo.created : undefined;
+
+ // For static context, isWorking is always false (turn is completed)
+ const context: TurnGroupingContextType = {
+ turnId: turn.turnId,
+ isFirstAssistantInTurn,
+ isLastAssistantInTurn,
+ summaryBody,
+ activityParts,
+ activityGroupSegments,
+ headerMessageId,
+ hasTools,
+ hasReasoning,
+ diffStats,
+ userMessageCreatedAt,
+ isWorking: false,
+ isGroupExpanded: uiState.isExpanded,
+ toggleGroup: () => uiStateData.toggleGroup(turn.turnId),
+ };
+
+ return context;
+ }, [staticData, uiStateData, messageId]);
+};
+
+interface TurnGroupingProviderProps {
+ messages: ChatMessageEntry[];
+ children: React.ReactNode;
+}
+
+const ACTIVITY_STANDALONE_TOOL_NAMES = new Set(['task']);
+
+const isActivityStandaloneTool = (toolName: unknown): boolean => {
+ return typeof toolName === 'string' && ACTIVITY_STANDALONE_TOOL_NAMES.has(toolName.toLowerCase());
+};
+
+const extractFinalAssistantText = (turn: Turn): string | undefined => {
+ for (let messageIndex = turn.assistantMessages.length - 1; messageIndex >= 0; messageIndex -= 1) {
+ const assistantMsg = turn.assistantMessages[messageIndex];
+ if (!assistantMsg) continue;
+
+ const infoFinish = (assistantMsg.info as { finish?: string | null | undefined }).finish;
+ if (infoFinish !== 'stop') continue;
+
+ for (let partIndex = assistantMsg.parts.length - 1; partIndex >= 0; partIndex -= 1) {
+ const part = assistantMsg.parts[partIndex];
+ if (!part || part.type !== 'text') continue;
+
+ const textContent = (part as { text?: string | null | undefined }).text ??
+ (part as { content?: string | null | undefined }).content;
+ if (typeof textContent === 'string' && textContent.trim().length > 0) {
+ return textContent;
+ }
+ }
+ }
+
+ return undefined;
+};
+
+const getTurnActivityInfo = (turn: Turn, showTextJustificationActivity: boolean): TurnActivityInfo => {
+ interface SummaryDiff {
+ additions?: number | null | undefined;
+ deletions?: number | null | undefined;
+ file?: string | null | undefined;
+ }
+ interface UserSummaryPayload {
+ body?: string | null | undefined;
+ diffs?: SummaryDiff[] | null | undefined;
+ }
+
+ const summaryBody = extractFinalAssistantText(turn);
+
+ let diffStats: TurnDiffStats | undefined;
+
+ const summary = (turn.userMessage.info as { summary?: UserSummaryPayload | null | undefined }).summary;
+ const diffs = summary?.diffs;
+ if (Array.isArray(diffs) && diffs.length > 0) {
+ let additions = 0;
+ let deletions = 0;
+ let files = 0;
+
+ diffs.forEach((diff) => {
+ if (!diff) return;
+ const diffAdditions = typeof diff.additions === 'number' ? diff.additions : 0;
+ const diffDeletions = typeof diff.deletions === 'number' ? diff.deletions : 0;
+ if (diffAdditions !== 0 || diffDeletions !== 0) {
+ files += 1;
+ }
+ additions += diffAdditions;
+ deletions += diffDeletions;
+ });
+
+ if (files > 0) {
+ diffStats = { additions, deletions, files };
+ }
+ }
+
+ let hasTools = false;
+ let hasReasoning = false;
+
+ turn.assistantMessages.forEach((msg) => {
+ msg.parts.forEach((part) => {
+ if (part.type === 'tool') hasTools = true;
+ else if (part.type === 'reasoning') hasReasoning = true;
+ });
+ });
+
+ // Find the LAST assistant message that has text content - this is the summary
+ // All other text messages are justification (yapping during work)
+ let lastTextMessageId: string | undefined;
+ for (let i = turn.assistantMessages.length - 1; i >= 0; i--) {
+ const msg = turn.assistantMessages[i];
+ if (!msg) continue;
+ const hasText = msg.parts.some((p) => {
+ if (p.type !== 'text') return false;
+ const text = (p as { text?: string; content?: string }).text ??
+ (p as { text?: string; content?: string }).content;
+ return typeof text === 'string' && text.trim().length > 0;
+ });
+ if (hasText) {
+ lastTextMessageId = msg.info.id;
+ break;
+ }
+ }
+
+ const activityParts: TurnActivityPart[] = [];
+
+ turn.assistantMessages.forEach((msg) => {
+ const messageId = msg.info.id;
+
+ // Only the LAST message with text is the summary (not justification)
+ // All earlier text messages are justification
+ const isFinalSummaryMessage = messageId === lastTextMessageId;
+
+ msg.parts.forEach((part, partIndex) => {
+ const baseId = `${messageId}-part-${partIndex}-${part.type}`;
+
+ if (part.type === 'tool') {
+ const state = (part as { state?: { time?: { end?: number | null | undefined } | null | undefined } | null | undefined }).state;
+ const time = state?.time;
+ const end = typeof time?.end === 'number' ? time.end : undefined;
+
+ activityParts.push({
+ id: baseId,
+ turnId: turn.turnId,
+ messageId,
+ kind: 'tool',
+ part,
+ endedAt: end,
+ });
+ return;
+ }
+
+ if (part.type === 'reasoning') {
+ const text = (part as { text?: string | null | undefined; content?: string | null | undefined }).text
+ ?? (part as { text?: string | null | undefined; content?: string | null | undefined }).content;
+ if (typeof text !== 'string' || text.trim().length === 0) return;
+ const time = (part as { time?: { end?: number | null | undefined } | null | undefined }).time;
+ const end = typeof time?.end === 'number' ? time.end : undefined;
+
+ activityParts.push({
+ id: baseId,
+ turnId: turn.turnId,
+ messageId,
+ kind: 'reasoning',
+ part,
+ endedAt: end,
+ });
+ return;
+ }
+
+ if (
+ showTextJustificationActivity &&
+ part.type === 'text' &&
+ (hasTools || hasReasoning) &&
+ !isFinalSummaryMessage
+ ) {
+ const text = (part as { text?: string | null | undefined; content?: string | null | undefined }).text ??
+ (part as { text?: string | null | undefined; content?: string | null | undefined }).content;
+ if (typeof text !== 'string' || text.trim().length === 0) return;
+ const time = (part as { time?: { end?: number | null | undefined } | null | undefined }).time;
+ const end = typeof time?.end === 'number' ? time.end : undefined;
+
+ activityParts.push({
+ id: baseId,
+ turnId: turn.turnId,
+ messageId,
+ kind: 'justification',
+ part,
+ endedAt: end,
+ });
+ }
+ });
+ });
+
+ const activityGroupSegments: TurnActivityGroup[] = [];
+ const activityByPart = new WeakMap();
+ activityParts.forEach((activity) => {
+ activityByPart.set(activity.part, activity);
+ });
+
+ const taskMessageById = new Map();
+ const taskOrder: string[] = [];
+ const partsByAfterTool = new Map();
+ let currentAfterToolPartId: string | null = null;
+
+ turn.assistantMessages.forEach((msg) => {
+ const messageId = msg.info.id;
+
+ msg.parts.forEach((part, partIndex) => {
+ if (part.type === 'tool') {
+ const toolName = (part as { tool?: unknown }).tool;
+ if (isActivityStandaloneTool(toolName)) {
+ const toolPartId = `${messageId}-part-${partIndex}-${part.type}`;
+
+ if (!taskMessageById.has(toolPartId)) {
+ taskMessageById.set(toolPartId, messageId);
+ taskOrder.push(toolPartId);
+ }
+ currentAfterToolPartId = toolPartId;
+ return;
+ }
+ }
+
+ const activity = activityByPart.get(part);
+ if (!activity) return;
+
+ if (activity.kind === 'tool') {
+ const toolName = (activity.part as { tool?: unknown }).tool;
+ if (isActivityStandaloneTool(toolName)) return;
+ }
+
+ const list = partsByAfterTool.get(currentAfterToolPartId) ?? [];
+ list.push(activity);
+ partsByAfterTool.set(currentAfterToolPartId, list);
+ });
+ });
+
+ const pickAnchorForStartSegment = (segmentParts: TurnActivityPart[]): string | undefined => {
+ if (segmentParts.length === 0) return undefined;
+
+ const countByMessage = new Map();
+ segmentParts.forEach((activity) => {
+ countByMessage.set(activity.messageId, (countByMessage.get(activity.messageId) ?? 0) + 1);
+ });
+
+ let firstWithAny: string | undefined;
+ let cumulative = 0;
+ for (const msg of turn.assistantMessages) {
+ const count = countByMessage.get(msg.info.id) ?? 0;
+ if (count > 0 && !firstWithAny) firstWithAny = msg.info.id;
+ cumulative += count;
+ if (cumulative >= 2) return msg.info.id;
+ }
+ return firstWithAny;
+ };
+
+ const orderedKeys: Array = [null, ...taskOrder];
+
+ orderedKeys.forEach((afterToolPartId) => {
+ const segmentParts = partsByAfterTool.get(afterToolPartId) ?? [];
+ if (segmentParts.length === 0) return;
+
+ const anchorMessageId = afterToolPartId === null
+ ? pickAnchorForStartSegment(segmentParts)
+ : taskMessageById.get(afterToolPartId);
+
+ if (!anchorMessageId) return;
+
+ activityGroupSegments.push({
+ id: `${turn.turnId}:${anchorMessageId}:${afterToolPartId ?? 'start'}`,
+ anchorMessageId,
+ afterToolPartId,
+ parts: segmentParts,
+ });
+ });
+
+ return {
+ activityParts,
+ activityGroupSegments,
+ hasTools,
+ hasReasoning,
+ summaryBody,
+ diffStats,
+ };
+};
+
+// Build neighbor lookup map from messages
+const buildNeighborMap = (messages: ChatMessageEntry[]): Map => {
+ const map = new Map();
+ messages.forEach((message, index) => {
+ map.set(message.info.id, {
+ previousMessage: index > 0 ? messages[index - 1] : undefined,
+ nextMessage: index < messages.length - 1 ? messages[index + 1] : undefined,
+ });
+ });
+ return map;
+};
+
+const getMessageRole = (message: ChatMessageEntry): string => {
+ const role = (message.info as { clientRole?: string | null | undefined }).clientRole ?? message.info.role;
+ return typeof role === 'string' ? role : '';
+};
+
+const getStructureKey = (messages: ChatMessageEntry[]): string => {
+ if (messages.length === 0) return '';
+ return messages
+ .map((message) => `${message.info?.id ?? ''}:${getMessageRole(message)}`)
+ .join('|');
+};
+
+export const TurnGroupingProvider: React.FC = ({ messages, children }) => {
+ const { isWorking: sessionIsWorking } = useCurrentSessionActivity();
+ const toolCallExpansion = useUIStore((state) => state.toolCallExpansion);
+ const showTextJustificationActivity = useUIStore((state) => state.showTextJustificationActivity);
+ const defaultActivityExpanded =
+ toolCallExpansion === 'activity' || toolCallExpansion === 'detailed' || toolCallExpansion === 'changes';
+ const structureKey = React.useMemo(() => getStructureKey(messages), [messages]);
+ const [structuredMessages, setStructuredMessages] = React.useState(messages);
+
+ React.useEffect(() => {
+ setStructuredMessages((previous) => {
+ if (getStructureKey(previous) === structureKey) {
+ return previous;
+ }
+ return messages;
+ });
+ }, [messages, structureKey]);
+
+ const staticStructureKey = React.useMemo(() => getStructureKey(structuredMessages), [structuredMessages]);
+
+ const staticValue = React.useMemo(() => {
+ const turns = detectTurns(structuredMessages);
+ const lastTurnId = turns.length > 0 ? turns[turns.length - 1]!.turnId : null;
+
+ const messageToTurn = new Map();
+ turns.forEach((turn) => {
+ messageToTurn.set(turn.userMessage.info.id, turn);
+ turn.assistantMessages.forEach((msg) => {
+ messageToTurn.set(msg.info.id, turn);
+ });
+ });
+
+ const turnActivityInfo = new Map();
+ turns.forEach((turn) => {
+ if (turn.turnId === lastTurnId) return;
+ turnActivityInfo.set(turn.turnId, getTurnActivityInfo(turn, showTextJustificationActivity));
+ });
+
+ const messageNeighbors = buildNeighborMap(structuredMessages);
+
+ const lastTurnMessageIds = new Set();
+ if (turns.length > 0) {
+ const lastTurn = turns[turns.length - 1]!;
+ lastTurnMessageIds.add(lastTurn.userMessage.info.id);
+ lastTurn.assistantMessages.forEach((msg) => {
+ lastTurnMessageIds.add(msg.info.id);
+ });
+ }
+
+ return {
+ structureKey: staticStructureKey,
+ turns,
+ messageToTurn,
+ turnActivityInfo,
+ lastTurnId,
+ lastTurnMessageIds,
+ defaultActivityExpanded,
+ messageNeighbors,
+ };
+ }, [defaultActivityExpanded, showTextJustificationActivity, staticStructureKey, structuredMessages]);
+
+ const lastTurnActivityInfo = React.useMemo(() => {
+ const lastTurnId = staticValue.lastTurnId;
+ if (!lastTurnId) return undefined;
+ // Find the last turn's user message in the current messages array to pick up
+ // streaming content changes without a second full detectTurns() pass.
+ const turns = staticValue.turns;
+ const lastTurn = turns.length > 0 ? turns[turns.length - 1] : undefined;
+ if (!lastTurn) return undefined;
+ // Re-slice assistant messages from the live `messages` array so that
+ // streamed part updates are reflected without re-detecting all turns.
+ const lastTurnUserId = lastTurn.userMessage.info.id;
+ const liveAssistant: ChatMessageEntry[] = [];
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
+ const candidate = messages[index];
+ if (!candidate) {
+ continue;
+ }
+
+ if (candidate.info.id === lastTurnUserId) {
+ break;
+ }
+
+ const role = (candidate.info as { clientRole?: string | null }).clientRole ?? candidate.info.role;
+ if (role === 'assistant') {
+ liveAssistant.push(candidate);
+ }
+ }
+
+ if (liveAssistant.length === 0 && messages.every((message) => message.info.id !== lastTurnUserId)) {
+ return getTurnActivityInfo(lastTurn, showTextJustificationActivity);
+ }
+
+ liveAssistant.reverse();
+ const liveTurn: Turn = { ...lastTurn, assistantMessages: liveAssistant };
+ return getTurnActivityInfo(liveTurn, showTextJustificationActivity);
+ }, [staticValue, messages, showTextJustificationActivity]);
+
+ // UI state for expansion toggles
+ const [turnUiStates, setTurnUiStates] = React.useState>(
+ () => new Map()
+ );
+
+ // Reset turn UI states when expansion preference changes
+ React.useEffect(() => {
+ setTurnUiStates(new Map());
+ }, [toolCallExpansion]);
+
+ const toggleGroup = React.useCallback((turnId: string) => {
+ setTurnUiStates((prev) => {
+ const next = new Map(prev);
+ const current = next.get(turnId) ?? { isExpanded: defaultActivityExpanded };
+ next.set(turnId, { isExpanded: !current.isExpanded });
+ return next;
+ });
+ }, [defaultActivityExpanded]);
+
+ // UI state - changes on user interaction (expand/collapse)
+ const uiStateValue = React.useMemo(() => ({
+ turnUiStates,
+ toggleGroup,
+ }), [turnUiStates, toggleGroup]);
+
+ // Streaming state - changes frequently during assistant response
+ const streamingValue = React.useMemo(() => ({
+ sessionIsWorking,
+ lastTurnActivityInfo,
+ }), [lastTurnActivityInfo, sessionIsWorking]);
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+};
+
+// Clear context cache on unmount or session change
+export const clearTurnGroupingCache = (): void => {
+ contextCache.clear();
+};
diff --git a/ui/src/components/chat/hooks/useTurnGrouping.ts b/ui/src/components/chat/hooks/useTurnGrouping.ts
new file mode 100644
index 0000000..0f8dd82
--- /dev/null
+++ b/ui/src/components/chat/hooks/useTurnGrouping.ts
@@ -0,0 +1,517 @@
+import React from 'react';
+import type { Message, Part } from '@opencode-ai/sdk/v2';
+import { useCurrentSessionActivity } from '@/hooks/useSessionActivity';
+import { useUIStore } from '@/stores/useUIStore';
+
+export interface ChatMessageEntry {
+ info: Message;
+ parts: Part[];
+}
+
+export interface Turn {
+ turnId: string;
+ userMessage: ChatMessageEntry;
+ assistantMessages: ChatMessageEntry[];
+}
+
+export type TurnActivityKind = 'tool' | 'reasoning' | 'justification';
+
+export interface TurnActivityPart {
+ id: string;
+ turnId: string;
+ messageId: string;
+ kind: TurnActivityKind;
+ part: Part;
+ endedAt?: number;
+}
+
+interface TurnDiffStats {
+ additions: number;
+ deletions: number;
+ files: number;
+}
+
+export interface TurnActivityGroup {
+ id: string;
+ anchorMessageId: string;
+ afterToolPartId: string | null;
+ parts: TurnActivityPart[];
+}
+
+export interface TurnGroupingContext {
+ turnId: string;
+ isFirstAssistantInTurn: boolean;
+ isLastAssistantInTurn: boolean;
+
+ summaryBody?: string;
+
+ activityParts: TurnActivityPart[];
+ activityGroupSegments: TurnActivityGroup[];
+ headerMessageId?: string;
+ hasTools: boolean;
+ hasReasoning: boolean;
+ diffStats?: TurnDiffStats;
+ userMessageCreatedAt?: number;
+
+ isWorking: boolean;
+ isGroupExpanded: boolean;
+
+ toggleGroup: () => void;
+}
+
+
+interface TurnUiState {
+ isExpanded: boolean;
+}
+
+interface TurnActivityInfo {
+ activityParts: TurnActivityPart[];
+ activityGroupSegments: TurnActivityGroup[];
+ hasTools: boolean;
+ hasReasoning: boolean;
+ summaryBody?: string;
+ diffStats?: TurnDiffStats;
+}
+
+const ACTIVITY_STANDALONE_TOOL_NAMES = new Set(['task']);
+
+const isActivityStandaloneTool = (toolName: unknown): boolean => {
+ return typeof toolName === 'string' && ACTIVITY_STANDALONE_TOOL_NAMES.has(toolName.toLowerCase());
+};
+
+export const detectTurns = (messages: ChatMessageEntry[]): Turn[] => {
+ const result: Turn[] = [];
+ let currentTurn: Turn | null = null;
+
+ messages.forEach((msg) => {
+ const role = (msg.info as { clientRole?: string | null | undefined }).clientRole ?? msg.info.role;
+
+ if (role === 'user') {
+ currentTurn = {
+ turnId: msg.info.id,
+ userMessage: msg,
+ assistantMessages: [],
+ };
+ result.push(currentTurn);
+ } else if (role === 'assistant' && currentTurn) {
+ currentTurn.assistantMessages.push(msg);
+ }
+ });
+
+ return result;
+};
+
+const extractFinalAssistantText = (turn: Turn): string | undefined => {
+ for (let messageIndex = turn.assistantMessages.length - 1; messageIndex >= 0; messageIndex -= 1) {
+ const assistantMsg = turn.assistantMessages[messageIndex];
+ if (!assistantMsg) continue;
+
+ const infoFinish = (assistantMsg.info as { finish?: string | null | undefined }).finish;
+ if (infoFinish !== 'stop') continue;
+
+ for (let partIndex = assistantMsg.parts.length - 1; partIndex >= 0; partIndex -= 1) {
+ const part = assistantMsg.parts[partIndex];
+ if (!part || part.type !== 'text') continue;
+
+ const textContent = (part as { text?: string | null | undefined }).text ??
+ (part as { content?: string | null | undefined }).content;
+ if (typeof textContent === 'string' && textContent.trim().length > 0) {
+ return textContent;
+ }
+ }
+ }
+
+ return undefined;
+};
+
+const getTurnActivityInfo = (turn: Turn, showTextJustificationActivity: boolean): TurnActivityInfo => {
+ interface SummaryDiff {
+ additions?: number | null | undefined;
+ deletions?: number | null | undefined;
+ file?: string | null | undefined;
+ }
+ interface UserSummaryPayload {
+ body?: string | null | undefined;
+ diffs?: SummaryDiff[] | null | undefined;
+ }
+
+ const summaryBody = extractFinalAssistantText(turn);
+
+ let diffStats: TurnDiffStats | undefined;
+
+ const summary = (turn.userMessage.info as { summary?: UserSummaryPayload | null | undefined }).summary;
+ const diffs = summary?.diffs;
+ if (Array.isArray(diffs) && diffs.length > 0) {
+ let additions = 0;
+ let deletions = 0;
+ let files = 0;
+
+ diffs.forEach((diff) => {
+ if (!diff) {
+ return;
+ }
+ const diffAdditions = typeof diff.additions === 'number' ? diff.additions : 0;
+ const diffDeletions = typeof diff.deletions === 'number' ? diff.deletions : 0;
+
+ if (diffAdditions !== 0 || diffDeletions !== 0) {
+ files += 1;
+ }
+ additions += diffAdditions;
+ deletions += diffDeletions;
+ });
+
+ if (files > 0) {
+ diffStats = {
+ additions,
+ deletions,
+ files,
+ };
+ }
+ }
+
+ let hasTools = false;
+ let hasReasoning = false;
+
+ turn.assistantMessages.forEach((msg) => {
+ msg.parts.forEach((part) => {
+ if (part.type === 'tool') {
+ hasTools = true;
+ } else if (part.type === 'reasoning') {
+ hasReasoning = true;
+ }
+ });
+ });
+
+ // Find the LAST assistant message that has text content - this is the summary
+ // All other text messages are justification (yapping during work)
+ let lastTextMessageId: string | undefined;
+ for (let i = turn.assistantMessages.length - 1; i >= 0; i--) {
+ const msg = turn.assistantMessages[i];
+ if (!msg) continue;
+ const hasText = msg.parts.some((p) => {
+ if (p.type !== 'text') return false;
+ const text = (p as { text?: string; content?: string }).text ??
+ (p as { text?: string; content?: string }).content;
+ return typeof text === 'string' && text.trim().length > 0;
+ });
+ if (hasText) {
+ lastTextMessageId = msg.info.id;
+ break;
+ }
+ }
+
+ const activityParts: TurnActivityPart[] = [];
+
+ turn.assistantMessages.forEach((msg) => {
+ const messageId = msg.info.id;
+
+ // Only the LAST message with text is the summary (not justification)
+ // All earlier text messages are justification
+ const isFinalSummaryMessage = messageId === lastTextMessageId;
+
+ msg.parts.forEach((part, partIndex) => {
+ const baseId = `${messageId}-part-${partIndex}-${part.type}`;
+
+ if (part.type === 'tool') {
+ const state = (part as { state?: { time?: { end?: number | null | undefined } | null | undefined } | null | undefined }).state;
+ const time = state?.time;
+ const end = typeof time?.end === 'number' ? time.end : undefined;
+
+ activityParts.push({
+ id: baseId,
+ turnId: turn.turnId,
+ messageId,
+ kind: 'tool',
+ part,
+ endedAt: end,
+ });
+ return;
+ }
+
+ if (part.type === 'reasoning') {
+ const text = (part as { text?: string | null | undefined; content?: string | null | undefined }).text
+ ?? (part as { text?: string | null | undefined; content?: string | null | undefined }).content;
+ if (typeof text !== 'string' || text.trim().length === 0) {
+ return;
+ }
+ const time = (part as { time?: { end?: number | null | undefined } | null | undefined }).time;
+ const end = typeof time?.end === 'number' ? time.end : undefined;
+
+ activityParts.push({
+ id: baseId,
+ turnId: turn.turnId,
+ messageId,
+ kind: 'reasoning',
+ part,
+ endedAt: end,
+ });
+ return;
+ }
+
+ if (
+ showTextJustificationActivity &&
+ part.type === 'text' &&
+ (hasTools || hasReasoning) &&
+ !isFinalSummaryMessage
+ ) {
+ const text =
+ (part as { text?: string | null | undefined; content?: string | null | undefined }).text ??
+ (part as { text?: string | null | undefined; content?: string | null | undefined }).content;
+ if (typeof text !== 'string' || text.trim().length === 0) {
+ return;
+ }
+ const time = (part as { time?: { end?: number | null | undefined } | null | undefined }).time;
+ const end = typeof time?.end === 'number' ? time.end : undefined;
+
+ activityParts.push({
+ id: baseId,
+ turnId: turn.turnId,
+ messageId,
+ kind: 'justification',
+ part,
+ endedAt: end,
+ });
+ }
+ });
+ });
+
+ const activityGroupSegments: TurnActivityGroup[] = [];
+
+ const activityByPart = new WeakMap();
+ activityParts.forEach((activity) => {
+ activityByPart.set(activity.part, activity);
+ });
+
+ const taskMessageById = new Map();
+ const taskOrder: string[] = [];
+ const partsByAfterTool = new Map();
+
+ let currentAfterToolPartId: string | null = null;
+
+ turn.assistantMessages.forEach((msg) => {
+ const messageId = msg.info.id;
+
+ msg.parts.forEach((part, partIndex) => {
+ if (part.type === 'tool') {
+ const toolName = (part as { tool?: unknown }).tool;
+ if (isActivityStandaloneTool(toolName)) {
+ const toolPartId = `${messageId}-part-${partIndex}-${part.type}`;
+
+ if (!taskMessageById.has(toolPartId)) {
+ taskMessageById.set(toolPartId, messageId);
+ taskOrder.push(toolPartId);
+ }
+
+ currentAfterToolPartId = toolPartId;
+ return;
+ }
+ }
+
+ const activity = activityByPart.get(part);
+ if (!activity) {
+ return;
+ }
+
+ if (activity.kind === 'tool') {
+ const toolName = (activity.part as { tool?: unknown }).tool;
+ if (isActivityStandaloneTool(toolName)) {
+ return;
+ }
+ }
+
+ const list = partsByAfterTool.get(currentAfterToolPartId) ?? [];
+ list.push(activity);
+ partsByAfterTool.set(currentAfterToolPartId, list);
+ });
+ });
+
+ const pickAnchorForStartSegment = (segmentParts: TurnActivityPart[]): string | undefined => {
+ if (segmentParts.length === 0) return undefined;
+
+ const countByMessage = new Map();
+ segmentParts.forEach((activity) => {
+ countByMessage.set(activity.messageId, (countByMessage.get(activity.messageId) ?? 0) + 1);
+ });
+
+ let firstWithAny: string | undefined;
+ let cumulative = 0;
+ for (const msg of turn.assistantMessages) {
+ const count = countByMessage.get(msg.info.id) ?? 0;
+ if (count > 0 && !firstWithAny) {
+ firstWithAny = msg.info.id;
+ }
+ cumulative += count;
+ if (cumulative >= 2) {
+ return msg.info.id;
+ }
+ }
+ return firstWithAny;
+ };
+
+ const orderedKeys: Array = [null, ...taskOrder];
+
+ orderedKeys.forEach((afterToolPartId) => {
+ const segmentParts = partsByAfterTool.get(afterToolPartId) ?? [];
+ if (segmentParts.length === 0) {
+ return;
+ }
+
+ const anchorMessageId = afterToolPartId === null
+ ? pickAnchorForStartSegment(segmentParts)
+ : taskMessageById.get(afterToolPartId);
+
+ if (!anchorMessageId) {
+ return;
+ }
+
+ activityGroupSegments.push({
+ id: `${turn.turnId}:${anchorMessageId}:${afterToolPartId ?? 'start'}`,
+ anchorMessageId,
+ afterToolPartId,
+ parts: segmentParts,
+ });
+ });
+
+ return {
+ activityParts,
+ activityGroupSegments,
+ hasTools,
+ hasReasoning,
+ summaryBody,
+ diffStats,
+ };
+};
+
+interface UseTurnGroupingResult {
+ turns: Turn[];
+ getTurnForMessage: (messageId: string) => Turn | undefined;
+ getContextForMessage: (messageId: string) => TurnGroupingContext | undefined;
+}
+
+export const useTurnGrouping = (messages: ChatMessageEntry[]): UseTurnGroupingResult => {
+ const { isWorking: sessionIsWorking } = useCurrentSessionActivity();
+ const showTextJustificationActivity = useUIStore((state) => state.showTextJustificationActivity);
+
+ const turns = React.useMemo(() => detectTurns(messages), [messages]);
+
+ const lastTurnId = React.useMemo(() => {
+ if (turns.length === 0) return null;
+ return turns[turns.length - 1]!.turnId;
+ }, [turns]);
+
+ const messageToTurn = React.useMemo(() => {
+ const map = new Map();
+ turns.forEach((turn) => {
+ map.set(turn.userMessage.info.id, turn);
+ turn.assistantMessages.forEach((msg) => {
+ map.set(msg.info.id, turn);
+ });
+ });
+ return map;
+ }, [turns]);
+
+ const turnActivityInfo = React.useMemo(() => {
+ const map = new Map();
+ turns.forEach((turn) => {
+ map.set(turn.turnId, getTurnActivityInfo(turn, showTextJustificationActivity));
+ });
+ return map;
+ }, [turns, showTextJustificationActivity]);
+
+ const [turnUiStates, setTurnUiStates] = React.useState>(
+ () => new Map()
+ );
+
+ const toolCallExpansion = useUIStore((state) => state.toolCallExpansion);
+ // Activity group is expanded for 'activity', 'detailed', and 'changes'; collapsed for 'collapsed'
+ const defaultActivityExpanded =
+ toolCallExpansion === 'activity' || toolCallExpansion === 'detailed' || toolCallExpansion === 'changes';
+
+ // Reset turn UI states when the expansion preference changes
+ // This ensures the setting takes precedence over manual toggles
+ React.useEffect(() => {
+ setTurnUiStates(new Map());
+ }, [toolCallExpansion]);
+
+ const getOrCreateTurnState = React.useCallback(
+ (turnId: string): TurnUiState => {
+ const existing = turnUiStates.get(turnId);
+ if (existing) return existing;
+ return { isExpanded: defaultActivityExpanded };
+ },
+ [turnUiStates, defaultActivityExpanded]
+ );
+
+ const toggleGroup = React.useCallback((turnId: string) => {
+ setTurnUiStates((prev) => {
+ const next = new Map(prev);
+ const current = next.get(turnId) ?? { isExpanded: defaultActivityExpanded };
+ next.set(turnId, { isExpanded: !current.isExpanded });
+ return next;
+ });
+ }, [defaultActivityExpanded]);
+
+ const getTurnForMessage = React.useCallback(
+ (messageId: string): Turn | undefined => {
+ return messageToTurn.get(messageId);
+ },
+ [messageToTurn]
+ );
+
+ const getContextForMessage = React.useCallback(
+ (messageId: string): TurnGroupingContext | undefined => {
+ const turn = messageToTurn.get(messageId);
+ if (!turn) return undefined;
+
+ const isAssistantMessage = turn.assistantMessages.some(
+ (msg) => msg.info.id === messageId
+ );
+ if (!isAssistantMessage) return undefined;
+
+ const activityInfo = turnActivityInfo.get(turn.turnId);
+ const activityParts = activityInfo?.activityParts ?? [];
+ const activityGroupSegments = activityInfo?.activityGroupSegments ?? [];
+ const hasTools = Boolean(activityInfo?.hasTools);
+ const hasReasoning = Boolean(activityInfo?.hasReasoning);
+ const summaryBody = activityInfo?.summaryBody;
+ const diffStats = activityInfo?.diffStats;
+
+ const firstAssistantId = turn.assistantMessages[0]?.info.id;
+ const isFirstAssistantInTurn = messageId === firstAssistantId;
+ const lastAssistantId = turn.assistantMessages[turn.assistantMessages.length - 1]?.info.id;
+ const isLastAssistantInTurn = messageId === lastAssistantId;
+ const headerMessageId = firstAssistantId;
+
+ const uiState = getOrCreateTurnState(turn.turnId);
+ const isTurnWorking = sessionIsWorking && lastTurnId === turn.turnId;
+
+ const userTimeInfo = turn.userMessage.info.time as { created?: number } | undefined;
+ const userMessageCreatedAt = typeof userTimeInfo?.created === 'number' ? userTimeInfo.created : undefined;
+
+ return {
+ turnId: turn.turnId,
+ isFirstAssistantInTurn,
+ isLastAssistantInTurn,
+ summaryBody,
+ activityParts,
+ activityGroupSegments,
+ headerMessageId,
+ hasTools,
+ hasReasoning,
+ diffStats,
+ userMessageCreatedAt,
+ isWorking: isTurnWorking,
+ isGroupExpanded: uiState.isExpanded,
+ toggleGroup: () => toggleGroup(turn.turnId),
+ } satisfies TurnGroupingContext;
+ },
+ [getOrCreateTurnState, lastTurnId, messageToTurn, sessionIsWorking, toggleGroup, turnActivityInfo]
+ );
+
+
+ return {
+ turns,
+ getTurnForMessage,
+ getContextForMessage,
+ };
+};
diff --git a/ui/src/components/chat/message/DiffViewToggle.tsx b/ui/src/components/chat/message/DiffViewToggle.tsx
new file mode 100644
index 0000000..dc88309
--- /dev/null
+++ b/ui/src/components/chat/message/DiffViewToggle.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { RiAlignJustify, RiLayoutColumnLine } from '@remixicon/react';
+
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+
+export type DiffViewMode = 'side-by-side' | 'unified';
+
+interface DiffViewToggleProps {
+ mode: DiffViewMode;
+ onModeChange: (mode: DiffViewMode) => void;
+ className?: string;
+}
+
+export const DiffViewToggle: React.FC = ({ mode, onModeChange, className }) => {
+ const handleClick = React.useCallback(
+ (event: React.MouseEvent) => {
+ event.stopPropagation();
+ onModeChange(mode === 'side-by-side' ? 'unified' : 'side-by-side');
+ },
+ [mode, onModeChange]
+ );
+
+ return (
+
+ {mode === 'side-by-side' ? (
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/ui/src/components/chat/message/FadeInOnReveal.tsx b/ui/src/components/chat/message/FadeInOnReveal.tsx
new file mode 100644
index 0000000..f3c06e8
--- /dev/null
+++ b/ui/src/components/chat/message/FadeInOnReveal.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+
+interface FadeInOnRevealProps {
+ children: React.ReactNode;
+ className?: string;
+ skipAnimation?: boolean;
+}
+
+const FADE_ANIMATION_ENABLED = true;
+
+// Context to allow parent components (like VirtualMessageList) to disable animations
+// for items entering the viewport due to scrolling rather than new content
+const FadeInDisabledContext = React.createContext(false);
+
+export const FadeInDisabledProvider: React.FC<{ disabled: boolean; children: React.ReactNode }> = ({ disabled, children }) => (
+
+ {children}
+
+);
+
+export const FadeInOnReveal: React.FC = ({ children, className, skipAnimation }) => {
+ const contextDisabled = React.useContext(FadeInDisabledContext);
+ const shouldSkip = skipAnimation || contextDisabled;
+ const [visible, setVisible] = React.useState(shouldSkip);
+
+ React.useEffect(() => {
+ if (!FADE_ANIMATION_ENABLED || shouldSkip) {
+ return;
+ }
+
+ let frame: number | null = null;
+
+ const enable = () => setVisible(true);
+
+ if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
+ frame = window.requestAnimationFrame(enable);
+ } else {
+ enable();
+ }
+
+ return () => {
+ if (
+ frame !== null &&
+ typeof window !== 'undefined' &&
+ typeof window.cancelAnimationFrame === 'function'
+ ) {
+ window.cancelAnimationFrame(frame);
+ }
+ };
+ }, [shouldSkip]);
+
+ if (!FADE_ANIMATION_ENABLED || shouldSkip) {
+ return <>{children}>;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
diff --git a/ui/src/components/chat/message/MessageBody.tsx b/ui/src/components/chat/message/MessageBody.tsx
new file mode 100644
index 0000000..703f8d4
--- /dev/null
+++ b/ui/src/components/chat/message/MessageBody.tsx
@@ -0,0 +1,1478 @@
+import React from 'react';
+import type { Part } from '@opencode-ai/sdk/v2';
+
+import UserTextPart from './parts/UserTextPart';
+import ToolPart from './parts/ToolPart';
+import ProgressiveGroup from './parts/ProgressiveGroup';
+import ReasoningPart from './parts/ReasoningPart';
+import { MessageFilesDisplay } from '../FileAttachment';
+import type { ToolPart as ToolPartType } from '@opencode-ai/sdk/v2';
+import type { StreamPhase, ToolPopupContent, AgentMentionInfo } from './types';
+import type { TurnGroupingContext } from '../hooks/useTurnGrouping';
+import { cn } from '@/lib/utils';
+import { isEmptyTextPart, extractTextContent } from './partUtils';
+import { FadeInOnReveal } from './FadeInOnReveal';
+import { Button } from '@/components/ui/button';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
+import { RiCheckLine, RiFileCopyLine, RiChatNewLine, RiArrowGoBackLine, RiGitBranchLine, RiHourglassLine, RiTimeLine, RiImageDownloadLine, RiLoader4Line } from '@remixicon/react';
+import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
+import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
+
+import { SimpleMarkdownRenderer } from '../MarkdownRenderer';
+import { useMessageStore } from '@/stores/messageStore';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { flattenAssistantTextParts } from '@/lib/messages/messageText';
+import { MULTIRUN_EXECUTION_FORK_PROMPT_META_TEXT } from '@/lib/messages/executionMeta';
+import { TextSelectionMenu } from './TextSelectionMenu';
+import { copyTextToClipboard } from '@/lib/clipboard';
+import { isVSCodeRuntime } from '@/lib/desktop';
+import { toPng } from 'html-to-image';
+import { toast } from '@/components/ui';
+import { formatTimestampForDisplay } from './timeFormat';
+
+type SubtaskPartLike = Part & {
+ type: 'subtask';
+ description?: unknown;
+ command?: unknown;
+ agent?: unknown;
+ prompt?: unknown;
+ taskSessionID?: unknown;
+ model?: {
+ providerID?: unknown;
+ modelID?: unknown;
+ };
+};
+
+type ShellActionPartLike = Part & {
+ type: 'text';
+ shellAction?: {
+ command?: unknown;
+ output?: unknown;
+ status?: unknown;
+ };
+};
+
+const isSubtaskPart = (part: Part): part is SubtaskPartLike => {
+ return part.type === 'subtask';
+};
+
+const isShellActionPart = (part: Part): part is ShellActionPartLike => {
+ const textPart = part as unknown as { type?: unknown; shellAction?: unknown };
+ return textPart.type === 'text' && typeof textPart.shellAction === 'object' && textPart.shellAction !== null;
+};
+
+const normalizeSubtaskModel = (model: SubtaskPartLike['model']): string | null => {
+ if (!model || typeof model !== 'object') return null;
+ const providerID = typeof model.providerID === 'string' ? model.providerID.trim() : '';
+ const modelID = typeof model.modelID === 'string' ? model.modelID.trim() : '';
+ if (!providerID || !modelID) return null;
+ return `${providerID}/${modelID}`;
+};
+
+const UserSubtaskPart: React.FC<{ part: SubtaskPartLike }> = ({ part }) => {
+ const [expanded, setExpanded] = React.useState(false);
+ const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
+
+ const description = typeof part.description === 'string' ? part.description.trim() : '';
+ const command = typeof part.command === 'string' ? part.command.trim() : '';
+ const agent = typeof part.agent === 'string' ? part.agent.trim() : '';
+ const prompt = typeof part.prompt === 'string' ? part.prompt.trim() : '';
+ const taskSessionID = typeof part.taskSessionID === 'string' ? part.taskSessionID.trim() : '';
+ const model = normalizeSubtaskModel(part.model);
+
+ return (
+
+
+ Delegated task
+ {command ? (
+
+ /{command}
+
+ ) : null}
+ {agent ? (
+
+ @{agent}
+
+ ) : null}
+ {model ? (
+
+ {model}
+
+ ) : null}
+
+
+ {description ? (
+
+ {description}
+
+ ) : null}
+
+ {prompt ? (
+
+
setExpanded((value) => !value)}
+ >
+ {expanded ? 'Hide prompt' : 'Show prompt'}
+
+ {expanded ? (
+
+ {prompt}
+
+ ) : null}
+
+ ) : null}
+
+ {taskSessionID ? (
+
+ {
+ void setCurrentSession(taskSessionID);
+ }}
+ >
+ Open subtask session
+
+
+ ) : null}
+
+ );
+};
+
+const UserShellActionPart: React.FC<{ part: ShellActionPartLike }> = ({ part }) => {
+ const [expanded, setExpanded] = React.useState(false);
+ const [copiedOutput, setCopiedOutput] = React.useState(false);
+ const copiedResetTimeoutRef = React.useRef(null);
+
+ const command = typeof part.shellAction?.command === 'string' ? part.shellAction.command.trim() : '';
+ const output = typeof part.shellAction?.output === 'string' ? part.shellAction.output : '';
+ const status = typeof part.shellAction?.status === 'string' ? part.shellAction.status.trim().toLowerCase() : '';
+ const hasOutput = output.trim().length > 0;
+
+ const clearCopiedResetTimeout = React.useCallback(() => {
+ if (copiedResetTimeoutRef.current !== null && typeof window !== 'undefined') {
+ window.clearTimeout(copiedResetTimeoutRef.current);
+ copiedResetTimeoutRef.current = null;
+ }
+ }, []);
+
+ React.useEffect(() => {
+ return () => {
+ clearCopiedResetTimeout();
+ };
+ }, [clearCopiedResetTimeout]);
+
+ const copyOutputToClipboard = React.useCallback(async () => {
+ if (!hasOutput) return;
+
+ const result = await copyTextToClipboard(output);
+ if (!result.ok) return;
+
+ clearCopiedResetTimeout();
+ setCopiedOutput(true);
+ if (typeof window !== 'undefined') {
+ copiedResetTimeoutRef.current = window.setTimeout(() => {
+ setCopiedOutput(false);
+ copiedResetTimeoutRef.current = null;
+ }, 2000);
+ }
+ }, [clearCopiedResetTimeout, hasOutput, output]);
+
+ return (
+
+
+ Shell command
+ {status ? (
+
+ {status}
+
+ ) : null}
+
+
+ {command ? (
+
+ {command}
+
+ ) : null}
+
+ {hasOutput ? (
+
+
+ setExpanded((value) => !value)}
+ >
+ {expanded ? 'Hide output' : 'Show output'}
+
+ {
+ void copyOutputToClipboard();
+ }}
+ aria-label={copiedOutput ? 'Copied' : 'Copy output'}
+ title={copiedOutput ? 'Copied' : 'Copy output'}
+ >
+ {copiedOutput ? : }
+
+
+ {expanded ? (
+
+ {output}
+
+ ) : null}
+
+ ) : null}
+
+ );
+};
+
+const formatTurnDuration = (durationMs: number): string => {
+ const totalSeconds = durationMs / 1000;
+ if (totalSeconds < 60) {
+ return `${totalSeconds.toFixed(1)}s`;
+ }
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = Math.round(totalSeconds % 60);
+ return `${minutes}m ${seconds}s`;
+};
+
+const ACTIVITY_STANDALONE_TOOL_NAMES = new Set(['task']);
+
+const isActivityStandaloneTool = (toolName: unknown): boolean => {
+ return typeof toolName === 'string' && ACTIVITY_STANDALONE_TOOL_NAMES.has(toolName.toLowerCase());
+};
+
+interface MessageBodyProps {
+ messageId: string;
+ parts: Part[];
+ isUser: boolean;
+ isMessageCompleted: boolean;
+ messageFinish?: string;
+ messageCompletedAt?: number;
+ messageCreatedAt?: number;
+
+ syntaxTheme: { [key: string]: React.CSSProperties };
+
+ isMobile: boolean;
+ hasTouchInput?: boolean;
+ copiedCode: string | null;
+ onCopyCode: (code: string) => void;
+ expandedTools: Set;
+ onToggleTool: (toolId: string) => void;
+ onShowPopup: (content: ToolPopupContent) => void;
+ streamPhase: StreamPhase;
+ allowAnimation: boolean;
+ onContentChange?: (reason?: ContentChangeReason, messageId?: string) => void;
+
+ shouldShowHeader?: boolean;
+ hasTextContent?: boolean;
+ onCopyMessage?: () => void;
+ copiedMessage?: boolean;
+ onAuxiliaryContentComplete?: () => void;
+ showReasoningTraces?: boolean;
+ agentMention?: AgentMentionInfo;
+ turnGroupingContext?: TurnGroupingContext;
+ onRevert?: () => void;
+ onFork?: () => void;
+ errorMessage?: string;
+ userActionsMode?: 'inline' | 'external-content' | 'external-actions';
+ stickyUserHeaderEnabled?: boolean;
+}
+
+const UserMessageBody: React.FC<{
+ messageId: string;
+ parts: Part[];
+ isMobile: boolean;
+ hasTouchInput?: boolean;
+ hasTextContent?: boolean;
+ onCopyMessage?: () => void;
+ copiedMessage?: boolean;
+ onShowPopup: (content: ToolPopupContent) => void;
+ agentMention?: AgentMentionInfo;
+ onRevert?: () => void;
+ onFork?: () => void;
+ userActionsMode?: 'inline' | 'external-content' | 'external-actions';
+ stickyUserHeaderEnabled?: boolean;
+}> = ({ messageId, parts, isMobile, hasTouchInput, hasTextContent, onCopyMessage, copiedMessage, onShowPopup, agentMention, onRevert, onFork, userActionsMode = 'inline', stickyUserHeaderEnabled = true }) => {
+ const [copyHintVisible, setCopyHintVisible] = React.useState(false);
+ const copyHintTimeoutRef = React.useRef(null);
+
+ const userContentParts = React.useMemo(() => {
+ return parts.filter((part) => {
+ if (part.type === 'text') {
+ return !isEmptyTextPart(part);
+ }
+ if (isSubtaskPart(part)) {
+ return true;
+ }
+ if (isShellActionPart(part)) {
+ return true;
+ }
+ return false;
+ });
+ }, [parts]);
+
+ const mentionToken = agentMention?.token;
+ let mentionInjected = false;
+
+ const canCopyMessage = Boolean(onCopyMessage);
+ const isMessageCopied = Boolean(copiedMessage);
+ const isTouchContext = Boolean(hasTouchInput ?? isMobile);
+ const hasCopyableText = Boolean(hasTextContent);
+ const showUserContent = userActionsMode !== 'external-actions';
+ const showUserActions = userActionsMode !== 'external-content';
+ const useStickyScrollableUserContent = stickyUserHeaderEnabled && userActionsMode === 'inline';
+
+ const clearCopyHintTimeout = React.useCallback(() => {
+ if (copyHintTimeoutRef.current !== null && typeof window !== 'undefined') {
+ window.clearTimeout(copyHintTimeoutRef.current);
+ copyHintTimeoutRef.current = null;
+ }
+ }, []);
+
+ const revealCopyHint = React.useCallback(() => {
+ if (!isTouchContext || !canCopyMessage || !hasCopyableText || typeof window === 'undefined') {
+ return;
+ }
+
+ clearCopyHintTimeout();
+ setCopyHintVisible(true);
+ copyHintTimeoutRef.current = window.setTimeout(() => {
+ setCopyHintVisible(false);
+ copyHintTimeoutRef.current = null;
+ }, 1800);
+ }, [canCopyMessage, clearCopyHintTimeout, hasCopyableText, isTouchContext]);
+
+ React.useEffect(() => {
+ if (!hasCopyableText) {
+ setCopyHintVisible(false);
+ clearCopyHintTimeout();
+ }
+ }, [clearCopyHintTimeout, hasCopyableText]);
+
+ const handleCopyButtonClick = React.useCallback(
+ (event: React.MouseEvent) => {
+ if (!onCopyMessage || !hasCopyableText) {
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ onCopyMessage();
+
+ if (isTouchContext) {
+ revealCopyHint();
+ }
+ },
+ [hasCopyableText, isTouchContext, onCopyMessage, revealCopyHint]
+ );
+
+ const actionsBlock = ((canCopyMessage && hasCopyableText) || onRevert || onFork) && showUserActions ? (
+
+
+ {onRevert && (
+
+
+ event.stopPropagation()}
+ onClick={(event) => {
+ event.stopPropagation();
+ onRevert();
+ }}
+ >
+
+
+
+ Revert from here
+
+ )}
+ {onFork && (
+
+
+ event.stopPropagation()}
+ onClick={(event) => {
+ event.stopPropagation();
+ onFork();
+ }}
+ >
+
+
+
+ Fork from here
+
+ )}
+ {canCopyMessage && hasCopyableText && (
+
+
+ event.stopPropagation()}
+ onClick={handleCopyButtonClick}
+ onFocus={() => setCopyHintVisible(true)}
+ onBlur={() => {
+ if (!isMessageCopied) {
+ setCopyHintVisible(false);
+ }
+ }}
+ >
+ {isMessageCopied ? (
+
+ ) : (
+
+ )}
+
+
+ Copy message
+
+ )}
+
+
+ ) : null;
+
+ if (!showUserContent) {
+ return <>{actionsBlock}>;
+ }
+
+ return (
+
+
+ {userContentParts.map((part, index) => {
+ if (isSubtaskPart(part)) {
+ return (
+
+
+
+ );
+ }
+
+ if (isShellActionPart(part)) {
+ return (
+
+
+
+ );
+ }
+
+ let mentionForPart: AgentMentionInfo | undefined;
+ if (agentMention && mentionToken && !mentionInjected) {
+ const candidateText = extractTextContent(part);
+ if (candidateText.includes(mentionToken)) {
+ mentionForPart = agentMention;
+ mentionInjected = true;
+ }
+ }
+ return (
+
+
+
+ );
+ })}
+
+
+ {actionsBlock}
+
+ );
+};
+
+const AssistantMessageBody: React.FC> = ({
+ messageId,
+ parts,
+ isMessageCompleted,
+ messageFinish,
+ messageCompletedAt,
+ messageCreatedAt,
+
+ syntaxTheme,
+ isMobile,
+ hasTouchInput,
+ expandedTools,
+ onToggleTool,
+ onShowPopup,
+ streamPhase: _streamPhase,
+ allowAnimation: _allowAnimation,
+ onContentChange,
+ hasTextContent = false,
+ onCopyMessage,
+ copiedMessage = false,
+ onAuxiliaryContentComplete,
+ showReasoningTraces = false,
+ turnGroupingContext,
+ errorMessage,
+}) => {
+
+ void _streamPhase;
+ void _allowAnimation;
+ const [copyHintVisible, setCopyHintVisible] = React.useState(false);
+ const copyHintTimeoutRef = React.useRef(null);
+ const messageContentRef = React.useRef(null);
+
+ const canCopyMessage = Boolean(onCopyMessage);
+ const isMessageCopied = Boolean(copiedMessage);
+ const isTouchContext = Boolean(hasTouchInput ?? isMobile);
+ const awaitingMessageCompletion = !isMessageCompleted;
+
+ const visibleParts = React.useMemo(() => {
+ return parts
+ .filter((part) => !isEmptyTextPart(part))
+ .filter((part) => {
+ const rawPart = part as Record;
+ return rawPart.type !== 'compaction';
+ });
+ }, [parts]);
+
+ const toolParts = React.useMemo(() => {
+ return visibleParts.filter((part): part is ToolPartType => part.type === 'tool');
+ }, [visibleParts]);
+
+ const assistantTextParts = React.useMemo(() => {
+ return visibleParts.filter((part) => part.type === 'text');
+ }, [visibleParts]);
+
+ const createSessionFromAssistantMessage = useSessionStore((state) => state.createSessionFromAssistantMessage);
+ const openMultiRunLauncherWithPrompt = useUIStore((state) => state.openMultiRunLauncherWithPrompt);
+ const isLastAssistantInTurn = turnGroupingContext?.isLastAssistantInTurn ?? false;
+ const hasStopFinish = messageFinish === 'stop';
+
+ const hasTools = toolParts.length > 0;
+
+ const hasPendingTools = React.useMemo(() => {
+ return toolParts.some((toolPart) => {
+ const state = (toolPart as Record).state as Record | undefined ?? {};
+ const status = state?.status;
+ return status === 'pending' || status === 'running' || status === 'started';
+ });
+ }, [toolParts]);
+
+ const isActiveTool = React.useCallback((toolPart: ToolPartType): boolean => {
+ const state = (toolPart as Record).state as Record | undefined ?? {};
+ const status = state?.status;
+ return status === 'pending' || status === 'running' || status === 'started';
+ }, []);
+
+ const isToolFinalized = React.useCallback((toolPart: ToolPartType) => {
+ const state = (toolPart as Record).state as Record | undefined ?? {};
+ const status = state?.status;
+ if (status === 'pending' || status === 'running' || status === 'started') {
+ return false;
+ }
+ const time = state?.time as Record | undefined ?? {};
+ const endTime = typeof time?.end === 'number' ? time.end : undefined;
+ const startTime = typeof time?.start === 'number' ? time.start : undefined;
+ if (typeof endTime !== 'number') {
+ return false;
+ }
+ if (typeof startTime === 'number' && endTime < startTime) {
+ return false;
+ }
+ return true;
+ }, []);
+
+ const shouldShowTool = React.useCallback((toolPart: ToolPartType): boolean => {
+ return isActiveTool(toolPart) || isToolFinalized(toolPart);
+ }, [isActiveTool, isToolFinalized]);
+
+ const allToolsFinalized = React.useMemo(() => {
+ if (toolParts.length === 0) {
+ return true;
+ }
+ if (hasPendingTools) {
+ return false;
+ }
+ return toolParts.every((toolPart) => isToolFinalized(toolPart));
+ }, [toolParts, hasPendingTools, isToolFinalized]);
+
+
+ const reasoningParts = React.useMemo(() => {
+ return visibleParts.filter((part) => part.type === 'reasoning');
+ }, [visibleParts]);
+
+ const reasoningComplete = React.useMemo(() => {
+ if (reasoningParts.length === 0) {
+ return true;
+ }
+ return reasoningParts.every((part) => {
+ const time = (part as Record).time as { end?: number } | undefined;
+ return typeof time?.end === 'number';
+ });
+ }, [reasoningParts]);
+
+ // Message is considered to have an "open step" if info.finish is not yet present
+ const hasOpenStep = typeof messageFinish !== 'string';
+
+ const shouldHoldForReasoning =
+ reasoningParts.length > 0 &&
+ hasTools &&
+ (hasPendingTools || hasOpenStep || !allToolsFinalized);
+
+
+ const shouldHoldTools = awaitingMessageCompletion
+ || (hasTools && (hasPendingTools || hasOpenStep || !allToolsFinalized));
+ const shouldHoldReasoning = awaitingMessageCompletion || shouldHoldForReasoning;
+
+ const hasAuxiliaryContent = hasTools || reasoningParts.length > 0;
+ const isTextlessAssistantMessage = assistantTextParts.length === 0;
+ const auxiliaryContentComplete = hasAuxiliaryContent && isTextlessAssistantMessage && !shouldHoldTools && !shouldHoldReasoning && allToolsFinalized && reasoningComplete;
+ const auxiliaryCompletionAnnouncedRef = React.useRef(false);
+ const soloReasoningScrollTriggeredRef = React.useRef(false);
+
+ React.useEffect(() => {
+ soloReasoningScrollTriggeredRef.current = false;
+ }, [messageId]);
+
+ React.useEffect(() => {
+ if (!auxiliaryContentComplete) {
+ auxiliaryCompletionAnnouncedRef.current = false;
+ return;
+ }
+ if (auxiliaryCompletionAnnouncedRef.current) {
+ return;
+ }
+ auxiliaryCompletionAnnouncedRef.current = true;
+ onAuxiliaryContentComplete?.();
+ }, [auxiliaryContentComplete, onAuxiliaryContentComplete]);
+
+ React.useEffect(() => {
+ if (awaitingMessageCompletion) {
+ soloReasoningScrollTriggeredRef.current = false;
+ return;
+ }
+ if (hasTools) {
+ soloReasoningScrollTriggeredRef.current = false;
+ return;
+ }
+ if (reasoningParts.length === 0) {
+ return;
+ }
+ if (shouldHoldReasoning || !reasoningComplete) {
+ return;
+ }
+ if (soloReasoningScrollTriggeredRef.current) {
+ return;
+ }
+ soloReasoningScrollTriggeredRef.current = true;
+ onContentChange?.('structural');
+ }, [awaitingMessageCompletion, hasTools, onContentChange, reasoningComplete, reasoningParts.length, shouldHoldReasoning]);
+
+ const hasCopyableText = Boolean(hasTextContent) && !awaitingMessageCompletion;
+
+ const clearCopyHintTimeout = React.useCallback(() => {
+ if (copyHintTimeoutRef.current !== null && typeof window !== 'undefined') {
+ window.clearTimeout(copyHintTimeoutRef.current);
+ copyHintTimeoutRef.current = null;
+ }
+ }, []);
+
+ const revealCopyHint = React.useCallback(() => {
+ if (!isTouchContext || !canCopyMessage || !hasCopyableText || typeof window === 'undefined') {
+ return;
+ }
+
+ clearCopyHintTimeout();
+ setCopyHintVisible(true);
+ copyHintTimeoutRef.current = window.setTimeout(() => {
+ setCopyHintVisible(false);
+ copyHintTimeoutRef.current = null;
+ }, 1800);
+ }, [canCopyMessage, clearCopyHintTimeout, hasCopyableText, isTouchContext]);
+
+ React.useEffect(() => {
+ if (!hasCopyableText) {
+ setCopyHintVisible(false);
+ clearCopyHintTimeout();
+ }
+ }, [clearCopyHintTimeout, hasCopyableText]);
+
+ const handleCopyButtonClick = React.useCallback(
+ (event: React.MouseEvent) => {
+ if (!onCopyMessage || !hasCopyableText) {
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ onCopyMessage();
+
+ if (isTouchContext) {
+ revealCopyHint();
+ }
+ },
+ [hasCopyableText, isTouchContext, onCopyMessage, revealCopyHint]
+ );
+
+ const handleForkClick = React.useCallback(
+ (event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ if (!createSessionFromAssistantMessage) {
+ return;
+ }
+ void createSessionFromAssistantMessage(messageId);
+ },
+ [createSessionFromAssistantMessage, messageId]
+ );
+
+ const handleForkMultiRunClick = React.useCallback(
+ (event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const assistantPlanText = flattenAssistantTextParts(assistantTextParts);
+ if (!assistantPlanText.trim()) {
+ return;
+ }
+
+ const prefilledPrompt = `${MULTIRUN_EXECUTION_FORK_PROMPT_META_TEXT}\n\n${assistantPlanText}`;
+ openMultiRunLauncherWithPrompt(prefilledPrompt);
+ },
+ [assistantTextParts, openMultiRunLauncherWithPrompt]
+ );
+
+ const [isSharing, setIsSharing] = React.useState(false);
+
+ const handleShareImage = React.useCallback(
+ async (event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ if (!messageContentRef.current || isSharing) return;
+
+ setIsSharing(true);
+ let wrapper: HTMLDivElement | null = null;
+ try {
+ const originalElement = messageContentRef.current;
+ const computedStyle = window.getComputedStyle(originalElement);
+ const rootStyle = window.getComputedStyle(document.documentElement);
+ const resolvedBackgroundColor =
+ rootStyle.getPropertyValue('--surface-background').trim() ||
+ computedStyle.backgroundColor ||
+ window.getComputedStyle(document.body).backgroundColor;
+ const paddingSize = 24;
+
+ wrapper = document.createElement('div');
+ wrapper.style.cssText = `
+ padding: ${paddingSize}px;
+ background-color: ${resolvedBackgroundColor};
+ display: inline-block;
+ `;
+
+ const clone = originalElement.cloneNode(true) as HTMLElement;
+ clone.style.cssText = `
+ ${computedStyle.cssText}
+ transform: none;
+ contain: none;
+ `;
+
+ const timestampElements = clone.querySelectorAll('[aria-label^="Message time:"]');
+ const footerRowsAdjusted = new Set();
+ timestampElements.forEach((element) => {
+ const label = element.getAttribute('aria-label');
+ const timestamp = label?.replace('Message time:', '').trim();
+ if (!timestamp || element.textContent?.includes(timestamp)) {
+ return;
+ }
+
+ const timestampText = document.createElement('span');
+ timestampText.style.marginLeft = '4px';
+ timestampText.textContent = timestamp;
+ element.appendChild(timestampText);
+
+ const metaGroup = element.parentElement;
+ const footerRow = metaGroup?.parentElement as HTMLElement | null;
+ const actionsGroup = footerRow?.firstElementChild as HTMLElement | null;
+ if (!footerRow || !actionsGroup || actionsGroup === metaGroup || footerRowsAdjusted.has(footerRow)) {
+ return;
+ }
+
+ actionsGroup.style.display = 'none';
+ footerRow.style.justifyContent = 'flex-start';
+ footerRowsAdjusted.add(footerRow);
+ });
+
+ wrapper.appendChild(clone);
+ document.body.appendChild(wrapper);
+
+ const dataUrl = await toPng(wrapper, {
+ quality: 1,
+ pixelRatio: 2,
+ backgroundColor: resolvedBackgroundColor,
+ });
+
+ const fileName = `message-${messageId}.png`;
+
+ if (isVSCodeRuntime()) {
+ const response = await fetch('/api/vscode/save-image', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ fileName, dataUrl }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to save image in VS Code');
+ }
+
+ const payload = await response.json() as { saved?: boolean; canceled?: boolean; error?: string };
+ if (payload.saved !== true) {
+ if (payload.canceled) {
+ return;
+ }
+ throw new Error(payload.error || 'Failed to save image in VS Code');
+ }
+ } else {
+ const link = document.createElement('a');
+ link.download = fileName;
+ link.href = dataUrl;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }
+
+ toast.success('Image saved');
+ } catch (error) {
+ console.error('Failed to generate image:', error);
+ toast.error('Failed to generate image');
+ } finally {
+ if (wrapper && wrapper.parentNode) {
+ wrapper.parentNode.removeChild(wrapper);
+ }
+ setIsSharing(false);
+ }
+ },
+ [messageId, isSharing]
+ );
+
+ React.useEffect(() => {
+ return () => {
+ clearCopyHintTimeout();
+ };
+ }, [clearCopyHintTimeout]);
+
+ const toolConnections = React.useMemo(() => {
+ const connections: Record = {};
+ const displayableTools = toolParts.filter((toolPart) => {
+ if (isActivityStandaloneTool(toolPart.tool)) {
+ return false;
+ }
+ return shouldShowTool(toolPart);
+ });
+
+ displayableTools.forEach((toolPart, index) => {
+ connections[toolPart.id] = {
+ hasPrev: index > 0,
+ hasNext: index < displayableTools.length - 1,
+ };
+ });
+
+ return connections;
+ }, [toolParts, shouldShowTool]);
+
+ const activityPartsForTurn = React.useMemo(() => {
+ return turnGroupingContext?.activityParts ?? [];
+ }, [turnGroupingContext]);
+
+ const activityPartsForMessage = React.useMemo(() => {
+ if (!turnGroupingContext) return [];
+ return activityPartsForTurn.filter((activity) => activity.messageId === messageId);
+ }, [activityPartsForTurn, messageId, turnGroupingContext]);
+
+ const activityGroupSegmentsForMessage = React.useMemo(() => {
+ if (!turnGroupingContext) return [];
+ return turnGroupingContext.activityGroupSegments.filter((segment) => segment.anchorMessageId === messageId);
+ }, [messageId, turnGroupingContext]);
+
+ const activityPartsByPart = React.useMemo(() => {
+ const map = new Map();
+ activityPartsForMessage.forEach((activity) => {
+ map.set(activity.part, activity);
+ });
+ return map;
+ }, [activityPartsForMessage]);
+
+
+ const visibleActivityPartsForTurn = React.useMemo(() => {
+ if (!turnGroupingContext) return [];
+
+ // Filter out reasoning if showReasoningTraces is off.
+ // Justification parts are already filtered at the source (useTurnGrouping)
+ // based on showTextJustificationActivity, so we keep them here.
+ const base = !showReasoningTraces
+ ? activityPartsForTurn.filter((activity) => activity.kind !== 'reasoning')
+ : activityPartsForTurn;
+
+ // Tools rendered standalone are excluded from Activity group.
+ return base.filter((activity) => {
+ if (activity.kind !== 'tool') {
+ return true;
+ }
+ const toolName = (activity.part as ToolPartType).tool;
+ return !isActivityStandaloneTool(toolName);
+ });
+ }, [activityPartsForTurn, showReasoningTraces, turnGroupingContext]);
+
+ const [hasEverHadMultipleVisibleActivities, setHasEverHadMultipleVisibleActivities] = React.useState(false);
+
+ React.useEffect(() => {
+ if (!turnGroupingContext) {
+ return;
+ }
+
+ const hasTaskSplitSegments = turnGroupingContext.activityGroupSegments.some(
+ (segment) => segment.afterToolPartId !== null
+ );
+
+ const hasReasoningActivity = visibleActivityPartsForTurn.some(
+ (activity) => activity.kind === 'reasoning'
+ );
+
+ if (
+ visibleActivityPartsForTurn.length > 1 ||
+ (hasTaskSplitSegments && visibleActivityPartsForTurn.length > 0) ||
+ hasReasoningActivity
+ ) {
+ setHasEverHadMultipleVisibleActivities(true);
+ }
+ }, [turnGroupingContext, visibleActivityPartsForTurn]);
+
+ const shouldShowActivityGroup = Boolean(turnGroupingContext && hasEverHadMultipleVisibleActivities);
+
+ const shouldRenderActivityGroup = Boolean(
+ turnGroupingContext &&
+ shouldShowActivityGroup &&
+ visibleActivityPartsForTurn.length > 0 &&
+ activityGroupSegmentsForMessage.length > 0
+ );
+
+ const standaloneToolParts = React.useMemo(() => {
+ return toolParts.filter((toolPart) => isActivityStandaloneTool(toolPart.tool));
+ }, [toolParts]);
+
+
+ const renderedParts = React.useMemo(() => {
+ const rendered: React.ReactNode[] = [];
+
+ const renderActivitySegments = (afterToolPartId: string | null) => {
+ if (!turnGroupingContext || !shouldRenderActivityGroup) {
+ return;
+ }
+
+ activityGroupSegmentsForMessage
+ .filter((segment) => (segment.afterToolPartId ?? null) === afterToolPartId)
+ .forEach((segment) => {
+ // Filter out reasoning if showReasoningTraces is off.
+ // Justification parts are already filtered at the source (useTurnGrouping)
+ // based on showTextJustificationActivity, so we keep them here.
+ const visibleSegmentParts = !showReasoningTraces
+ ? segment.parts.filter((activity) => activity.kind !== 'reasoning')
+ : segment.parts;
+
+ if (visibleSegmentParts.length === 0) {
+ return;
+ }
+
+ rendered.push(
+
+ );
+ });
+ };
+
+ // Activity groups and standalone tasks are interleaved in message order.
+ // Note: Reasoning parts are rendered in the visibleParts.forEach loop below
+ // when Activity group isn't showing, to maintain proper ordering with tools.
+ renderActivitySegments(null);
+
+ standaloneToolParts.forEach((standaloneToolPart) => {
+ rendered.push(
+
+
+
+ );
+
+ renderActivitySegments(standaloneToolPart.id);
+ });
+
+ const partsWithTime: Array<{
+ part: Part;
+ index: number;
+ endTime: number | null;
+ element: React.ReactNode;
+ }> = [];
+
+ visibleParts.forEach((part, index) => {
+ const activity = activityPartsByPart.get(part);
+ if (!activity) {
+ return;
+ }
+
+ if (!turnGroupingContext) {
+ return;
+ }
+
+ let endTime: number | null = null;
+ let element: React.ReactNode | null = null;
+
+ if (!shouldShowActivityGroup) {
+ if (activity.kind === 'tool') {
+ const toolPart = part as ToolPartType;
+
+ if (isActivityStandaloneTool(toolPart.tool)) {
+ return;
+ }
+
+ const toolState = (toolPart as { state?: { time?: { end?: number | null | undefined } | null | undefined } | null | undefined }).state;
+ const time = toolState?.time;
+ const isFinalized = isToolFinalized(toolPart);
+
+ if (!shouldShowTool(toolPart)) {
+ return;
+ }
+
+ const connection = toolConnections[toolPart.id];
+
+ const toolElement = (
+
+
+
+ );
+
+ element = toolElement;
+ endTime = isFinalized && typeof time?.end === 'number' ? time.end : null;
+ } else if (activity.kind === 'reasoning' && showReasoningTraces) {
+ // Fallback rendering for reasoning when Activity group isn't shown
+ const time = (part as { time?: { end?: number | null | undefined } | null | undefined }).time;
+ const partEndTime = typeof time?.end === 'number' ? time.end : null;
+
+ const reasoningElement = (
+
+
+
+ );
+
+ element = reasoningElement;
+ endTime = partEndTime;
+ }
+
+ if (element) {
+ partsWithTime.push({
+ part,
+ index,
+ endTime,
+ element,
+ });
+ }
+ }
+ });
+
+ partsWithTime.sort((a, b) => {
+ if (a.endTime === null && b.endTime === null) {
+ return a.index - b.index;
+ }
+ if (a.endTime === null) {
+ return 1;
+ }
+ if (b.endTime === null) {
+ return -1;
+ }
+ return a.endTime - b.endTime;
+ });
+
+ partsWithTime.forEach(({ element }) => {
+ rendered.push(element);
+ });
+
+ return rendered;
+ }, [
+ activityPartsByPart,
+ activityGroupSegmentsForMessage,
+ expandedTools,
+ isMobile,
+ isToolFinalized,
+ messageId,
+ onContentChange,
+ onShowPopup,
+ onToggleTool,
+ shouldShowTool,
+ shouldShowActivityGroup,
+ showReasoningTraces,
+ syntaxTheme,
+ toolConnections,
+ turnGroupingContext,
+ visibleParts,
+ standaloneToolParts,
+ shouldRenderActivityGroup,
+ ]);
+
+ const userMessageId = turnGroupingContext?.turnId;
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+
+ const rawSummaryBodyFromStore = useMessageStore((state) => {
+ if (!userMessageId || !currentSessionId) return undefined;
+ const sessionMessages = state.messages.get(currentSessionId);
+ if (!sessionMessages) return undefined;
+ const userMsg = sessionMessages.find((m) => m.info?.id === userMessageId);
+ if (!userMsg) return undefined;
+ const summary = (userMsg.info as { summary?: { body?: string | null | undefined } | null | undefined }).summary;
+ const body = summary?.body;
+ return typeof body === 'string' && body.trim().length > 0 ? body : undefined;
+ });
+
+ const summaryCandidate =
+ typeof turnGroupingContext?.summaryBody === 'string' && turnGroupingContext.summaryBody.trim().length > 0
+ ? turnGroupingContext.summaryBody
+ : rawSummaryBodyFromStore;
+
+ const summaryBodyRef = React.useRef(undefined);
+ if (summaryCandidate && summaryCandidate.trim().length > 0) {
+ summaryBodyRef.current = summaryCandidate;
+ }
+ const prevUserMessageId = React.useRef(userMessageId);
+ if (prevUserMessageId.current !== userMessageId) {
+ prevUserMessageId.current = userMessageId;
+ summaryBodyRef.current = undefined;
+ }
+ const summaryBody = summaryBodyRef.current;
+
+ const showSummaryBody =
+ turnGroupingContext?.isLastAssistantInTurn &&
+ summaryBody &&
+ summaryBody.trim().length > 0;
+
+ const showErrorMessage = Boolean(errorMessage);
+
+ const shouldShowFooter = isLastAssistantInTurn && hasTextContent && (hasStopFinish || Boolean(errorMessage));
+
+ const turnDurationText = React.useMemo(() => {
+ if (!isLastAssistantInTurn || !hasStopFinish) return undefined;
+ const userCreatedAt = turnGroupingContext?.userMessageCreatedAt;
+ if (typeof userCreatedAt !== 'number' || typeof messageCompletedAt !== 'number') return undefined;
+ if (messageCompletedAt <= userCreatedAt) return undefined;
+ return formatTurnDuration(messageCompletedAt - userCreatedAt);
+ }, [isLastAssistantInTurn, hasStopFinish, turnGroupingContext?.userMessageCreatedAt, messageCompletedAt]);
+
+ const footerTimestamp = React.useMemo(() => {
+ const timestamp = typeof messageCompletedAt === 'number' && messageCompletedAt > 0
+ ? messageCompletedAt
+ : (typeof messageCreatedAt === 'number' && messageCreatedAt > 0 ? messageCreatedAt : null);
+ if (timestamp === null) return null;
+
+ const formatted = formatTimestampForDisplay(timestamp);
+ return formatted.length > 0 ? formatted : null;
+ }, [messageCompletedAt, messageCreatedAt]);
+
+ const footerTimestampClassName = 'text-sm text-muted-foreground/60 tabular-nums flex items-center gap-1';
+
+ const footerButtons = (
+ <>
+ {onCopyMessage && (
+
+
+ event.stopPropagation()}
+ onClick={handleCopyButtonClick}
+ onFocus={() => {
+ if (hasCopyableText) {
+ setCopyHintVisible(true);
+ }
+ }}
+ onBlur={() => {
+ if (!isMessageCopied) {
+ setCopyHintVisible(false);
+ }
+ }}
+ >
+ {isMessageCopied ? (
+
+ ) : (
+
+ )}
+
+
+ Copy answer
+
+ )}
+
+
+ event.stopPropagation()}
+ onClick={handleShareImage}
+ >
+ {isSharing ? (
+
+ ) : (
+
+ )}
+
+
+ {isSharing ? 'Saving image...' : 'Save as image'}
+
+
+
+ event.stopPropagation()}
+ onClick={handleForkClick}
+ >
+
+
+
+ Start new session from this answer
+
+
+
+ event.stopPropagation()}
+ onClick={handleForkMultiRunClick}
+ >
+
+
+
+ Start new multi-run from this answer
+
+ >
+ );
+
+ return (
+
+
+
+
+
+ {renderedParts}
+ {showErrorMessage && (
+
+
+
+
+
+ )}
+ {showSummaryBody && (
+
+
+
+ {shouldShowFooter && (
+
+
+ {footerButtons}
+
+
+ {turnDurationText ? (
+
+
+ {turnDurationText}
+
+ ) : null}
+ {footerTimestamp ? (
+
+
+ {footerTimestamp}
+
+ ) : null}
+
+
+ )}
+
+
+ )}
+
+
+ {!showSummaryBody && shouldShowFooter && (
+
+
+ {footerButtons}
+
+
+ {turnDurationText ? (
+
+
+ {turnDurationText}
+
+ ) : null}
+ {footerTimestamp ? (
+
+
+ {footerTimestamp}
+
+ ) : null}
+
+
+ )}
+
+
+
+ );
+};
+
+const MessageBody: React.FC = ({ isUser, ...props }) => {
+
+ if (isUser) {
+ return (
+
+ );
+ }
+
+ return ;
+};
+
+export default React.memo(MessageBody);
diff --git a/ui/src/components/chat/message/MessageHeader.tsx b/ui/src/components/chat/message/MessageHeader.tsx
new file mode 100644
index 0000000..1673121
--- /dev/null
+++ b/ui/src/components/chat/message/MessageHeader.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { RiAiAgentLine, RiBrainAi3Line, RiUser3Line } from '@remixicon/react';
+import { cn } from '@/lib/utils';
+import { getAgentColor } from '@/lib/agentColors';
+import { FadeInOnReveal } from './FadeInOnReveal';
+import { useProviderLogo } from '@/hooks/useProviderLogo';
+
+interface MessageHeaderProps {
+ isUser: boolean;
+ providerID: string | null;
+ agentName: string | undefined;
+ modelName: string | undefined;
+ variant?: string;
+ isDarkTheme: boolean;
+}
+
+const MessageHeader: React.FC = ({ isUser, providerID, agentName, modelName, variant, isDarkTheme }) => {
+ const { src: logoSrc, onError: handleLogoError, hasLogo } = useProviderLogo(providerID);
+
+ return (
+
+
+
+
+
+ {isUser ? (
+
+
+
+ ) : (
+
+ {hasLogo && logoSrc ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+ {isUser ? 'You' : (modelName || 'Assistant')}
+
+ {!isUser && agentName && (
+
+
+ {agentName}
+
+ )}
+ {!isUser && variant && (
+
+
+ {variant.length > 0 ? variant[0].toLowerCase() + variant.slice(1) : variant}
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default React.memo(MessageHeader);
diff --git a/ui/src/components/chat/message/TextSelectionMenu.tsx b/ui/src/components/chat/message/TextSelectionMenu.tsx
new file mode 100644
index 0000000..2487908
--- /dev/null
+++ b/ui/src/components/chat/message/TextSelectionMenu.tsx
@@ -0,0 +1,413 @@
+import React from 'react';
+import { createPortal } from 'react-dom';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { RiChatNewLine, RiAddLine, RiFileCopyLine } from '@remixicon/react';
+import { cn } from '@/lib/utils';
+import { copyTextToClipboard } from '@/lib/clipboard';
+
+interface TextSelectionMenuProps {
+ containerRef: React.RefObject;
+}
+
+interface MenuPosition {
+ x: number;
+ y: number;
+ show: boolean;
+}
+
+const DESKTOP_MENU_SIDE_MARGIN_PX = 8;
+const DESKTOP_MENU_FALLBACK_WIDTH_PX = 280;
+
+export const TextSelectionMenu: React.FC = ({ containerRef }) => {
+ const [position, setPosition] = React.useState({ x: 0, y: 0, show: false });
+ const [selectedText, setSelectedText] = React.useState('');
+ const [isDragging, setIsDragging] = React.useState(false);
+ const [isOpening, setIsOpening] = React.useState(false);
+ const menuRef = React.useRef(null);
+ const menuWidthRef = React.useRef(DESKTOP_MENU_FALLBACK_WIDTH_PX);
+ const pendingSelectionRef = React.useRef<{ text: string; rect: DOMRect } | null>(null);
+ const openRafRef = React.useRef(null);
+ const isMenuVisibleRef = React.useRef(false);
+ const createSession = useSessionStore((state) => state.createSession);
+ const setPendingInputText = useSessionStore((state) => state.setPendingInputText);
+ const isMobile = useUIStore((state) => state.isMobile);
+
+ React.useEffect(() => {
+ isMenuVisibleRef.current = position.show;
+ }, [position.show]);
+
+ React.useEffect(() => {
+ return () => {
+ if (openRafRef.current !== null) {
+ window.cancelAnimationFrame(openRafRef.current);
+ openRafRef.current = null;
+ }
+ };
+ }, []);
+
+ const hideMenu = React.useCallback(() => {
+ pendingSelectionRef.current = null;
+
+ if (!isMenuVisibleRef.current) {
+ return;
+ }
+
+ if (openRafRef.current !== null) {
+ window.cancelAnimationFrame(openRafRef.current);
+ openRafRef.current = null;
+ }
+ setIsOpening(false);
+
+ setPosition((prev) => ({ ...prev, show: false }));
+ setSelectedText('');
+ isMenuVisibleRef.current = false;
+ }, []);
+
+ const getDesktopClampedX = React.useCallback((anchorX: number) => {
+ if (typeof window === 'undefined') {
+ return anchorX;
+ }
+
+ const viewportWidth = window.innerWidth;
+ const menuWidth = menuWidthRef.current;
+ const halfWidth = menuWidth / 2;
+ const minX = DESKTOP_MENU_SIDE_MARGIN_PX + halfWidth;
+ const maxX = viewportWidth - DESKTOP_MENU_SIDE_MARGIN_PX - halfWidth;
+
+ if (minX > maxX) {
+ return viewportWidth / 2;
+ }
+
+ return Math.min(Math.max(anchorX, minX), maxX);
+ }, []);
+
+ const showMenu = React.useCallback(() => {
+ if (!pendingSelectionRef.current) return;
+
+ const { text, rect } = pendingSelectionRef.current;
+ const shouldAnimateIn = !position.show;
+
+ // Position menu above the selection
+ const menuX = isMobile
+ ? rect.left + rect.width / 2
+ : getDesktopClampedX(rect.left + rect.width / 2);
+ const menuY = rect.top - 10;
+
+ setSelectedText(text);
+ setPosition({
+ x: menuX,
+ y: menuY,
+ show: true,
+ });
+ isMenuVisibleRef.current = true;
+
+ if (shouldAnimateIn) {
+ setIsOpening(true);
+ if (openRafRef.current !== null) {
+ window.cancelAnimationFrame(openRafRef.current);
+ }
+ openRafRef.current = window.requestAnimationFrame(() => {
+ setIsOpening(false);
+ openRafRef.current = null;
+ });
+ }
+ }, [getDesktopClampedX, isMobile, position.show]);
+
+ React.useLayoutEffect(() => {
+ if (!position.show || isMobile || !menuRef.current) {
+ return;
+ }
+
+ const measuredWidth = menuRef.current.offsetWidth;
+ if (!Number.isFinite(measuredWidth) || measuredWidth <= 0 || measuredWidth === menuWidthRef.current) {
+ return;
+ }
+
+ menuWidthRef.current = measuredWidth;
+ setPosition((prev) => ({
+ ...prev,
+ x: getDesktopClampedX(prev.x),
+ }));
+ }, [getDesktopClampedX, isMobile, position.show]);
+
+ React.useEffect(() => {
+ if (!position.show || isMobile) {
+ return;
+ }
+
+ const handleViewportResize = () => {
+ setPosition((prev) => ({
+ ...prev,
+ x: getDesktopClampedX(prev.x),
+ }));
+ };
+
+ window.addEventListener('resize', handleViewportResize);
+ return () => {
+ window.removeEventListener('resize', handleViewportResize);
+ };
+ }, [getDesktopClampedX, isMobile, position.show]);
+
+ const handleSelectionChange = React.useCallback(() => {
+ const selection = window.getSelection();
+ const container = containerRef.current;
+
+ if (!selection || !container) {
+ if (!isDragging) {
+ hideMenu();
+ }
+ return;
+ }
+
+ const text = selection.toString().trim();
+
+ // Only show if we have text and the selection is within our container
+ if (!text) {
+ if (!isDragging) {
+ hideMenu();
+ }
+ return;
+ }
+
+ // Check if selection is within the container
+ const range = selection.getRangeAt(0);
+
+ if (!container.contains(range.commonAncestorContainer)) {
+ if (!isDragging) {
+ hideMenu();
+ }
+ return;
+ }
+
+ // Get selection coordinates
+ const rect = range.getBoundingClientRect();
+
+ // Store the selection but don't show menu yet if dragging
+ pendingSelectionRef.current = { text, rect };
+
+ // Only show menu if we're not currently dragging
+ if (!isDragging) {
+ showMenu();
+ }
+ }, [containerRef, hideMenu, showMenu, isDragging]);
+
+ React.useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ // Track when dragging starts
+ const handleMouseDown = () => {
+ setIsDragging(true);
+ hideMenu();
+ };
+
+ // Track when dragging stops
+ const handleMouseUp = () => {
+ setIsDragging(false);
+ // Check if we have a pending selection to show
+ if (pendingSelectionRef.current) {
+ // Small delay to ensure selection is finalized
+ setTimeout(() => {
+ const selection = window.getSelection();
+ if (selection && selection.toString().trim()) {
+ showMenu();
+ } else {
+ hideMenu();
+ }
+ }, 10);
+ }
+ };
+
+ // Listen for selection changes during drag
+ document.addEventListener('selectionchange', handleSelectionChange);
+
+ container.addEventListener('mousedown', handleMouseDown);
+ document.addEventListener('mouseup', handleMouseUp);
+
+ // Hide menu when clicking outside
+ const handleClickOutside = (e: MouseEvent) => {
+ if (
+ menuRef.current &&
+ !menuRef.current.contains(e.target as Node) &&
+ !window.getSelection()?.toString().trim()
+ ) {
+ hideMenu();
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+
+ return () => {
+ document.removeEventListener('selectionchange', handleSelectionChange);
+ container.removeEventListener('mousedown', handleMouseDown);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [containerRef, handleSelectionChange, hideMenu, showMenu]);
+
+ const handleAddToChat = React.useCallback(() => {
+ if (!selectedText) return;
+
+ setPendingInputText(selectedText, 'append');
+
+ hideMenu();
+
+ // Clear selection
+ window.getSelection()?.removeAllRanges();
+ }, [selectedText, setPendingInputText, hideMenu]);
+
+ const handleCreateNewSession = React.useCallback(async () => {
+ if (!selectedText) return;
+
+ const session = await createSession(undefined, null, null);
+ if (session) {
+ setPendingInputText(selectedText, 'replace');
+ }
+
+ hideMenu();
+ window.getSelection()?.removeAllRanges();
+ }, [selectedText, createSession, setPendingInputText, hideMenu]);
+
+ const handleCopy = React.useCallback(async () => {
+ if (!selectedText) return;
+
+ const result = await copyTextToClipboard(selectedText);
+ if (!result.ok) {
+ console.error('Failed to copy:', result.error);
+ }
+
+ hideMenu();
+ window.getSelection()?.removeAllRanges();
+ }, [selectedText, hideMenu]);
+
+ if (!position.show) return null;
+
+ // Mobile: Show as a bar at the bottom of the screen, above the keyboard
+ if (isMobile) {
+ return createPortal(
+
+
+
+ Add to chat
+
+
+
+
+ New session
+
+
+
+
+ Copy
+
+
,
+ document.body
+ );
+ }
+
+ // Desktop: Show as a popup above the selection
+ return createPortal(
+
+
+
+
+ Add to chat
+
+
+
+
+
+
+ New session
+
+
+
,
+ document.body
+ );
+};
+
+export default TextSelectionMenu;
diff --git a/ui/src/components/chat/message/ToolOutputDialog.tsx b/ui/src/components/chat/message/ToolOutputDialog.tsx
new file mode 100644
index 0000000..8ad9930
--- /dev/null
+++ b/ui/src/components/chat/message/ToolOutputDialog.tsx
@@ -0,0 +1,1214 @@
+import React from 'react';
+import { Dialog, DialogContent } from '@/components/ui/dialog';
+import { RiArrowLeftSLine, RiArrowRightSLine, RiBrainAi3Line, RiCloseLine, RiFileImageLine, RiFileList2Line, RiFilePdfLine, RiFileSearchLine, RiFolder6Line, RiGitBranchLine, RiGlobalLine, RiListCheck3, RiLoader4Line, RiPencilAiLine, RiSearchLine, RiTaskLine, RiTerminalBoxLine, RiToolsLine } from '@remixicon/react';
+import { File as PierreFile, PatchDiff } from '@pierre/diffs/react';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { createPortal } from 'react-dom';
+
+import { cn } from '@/lib/utils';
+import { SimpleMarkdownRenderer } from '../MarkdownRenderer';
+import { toolDisplayStyles } from '@/lib/typography';
+import { getLanguageFromExtension } from '@/lib/toolHelpers';
+import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
+import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
+import { getDefaultTheme } from '@/lib/theme/themes';
+import {
+ renderTodoOutput,
+ renderListOutput,
+ renderGrepOutput,
+ renderGlobOutput,
+ renderWebSearchOutput,
+ formatInputForDisplay,
+ parseReadToolOutput,
+} from './toolRenderers';
+import type { ToolPopupContent, DiffViewMode } from './types';
+import { DiffViewToggle } from './DiffViewToggle';
+import { VirtualizedCodeBlock, type CodeLine } from './parts/VirtualizedCodeBlock';
+
+interface ToolOutputDialogProps {
+ popup: ToolPopupContent;
+ onOpenChange: (open: boolean) => void;
+ syntaxTheme: { [key: string]: React.CSSProperties };
+ isMobile: boolean;
+}
+
+const getToolIcon = (toolName: string) => {
+ const iconClass = 'h-3.5 w-3.5 flex-shrink-0';
+ const tool = toolName.toLowerCase();
+
+ if (tool === 'reasoning') {
+ return ;
+ }
+ if (tool === 'image-preview') {
+ return ;
+ }
+ if (tool === 'mermaid-preview') {
+ return ;
+ }
+ if (tool === 'edit' || tool === 'multiedit' || tool === 'apply_patch' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
+ return ;
+ }
+ if (tool === 'write' || tool === 'create' || tool === 'file_write') {
+ return ;
+ }
+ if (tool === 'read' || tool === 'view' || tool === 'file_read' || tool === 'cat') {
+ return ;
+ }
+ if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal') {
+ return ;
+ }
+ if (tool === 'list' || tool === 'ls' || tool === 'dir' || tool === 'list_files') {
+ return ;
+ }
+ if (tool === 'search' || tool === 'grep' || tool === 'find' || tool === 'ripgrep') {
+ return ;
+ }
+ if (tool === 'glob') {
+ return ;
+ }
+ if (tool === 'fetch' || tool === 'curl' || tool === 'wget' || tool === 'webfetch') {
+ return ;
+ }
+ if (tool === 'web-search' || tool === 'websearch' || tool === 'search_web' || tool === 'google' || tool === 'bing' || tool === 'duckduckgo') {
+ return ;
+ }
+ if (tool === 'todowrite' || tool === 'todoread') {
+ return ;
+ }
+ if (tool === 'plan_enter') {
+ return ;
+ }
+ if (tool === 'plan_exit') {
+ return ;
+ }
+ if (tool.startsWith('git')) {
+ return ;
+ }
+ return ;
+};
+
+const PREVIEW_ANIMATION_MS = 150;
+const MERMAID_DIALOG_HEADER_HEIGHT = 40;
+const MERMAID_ASPECT_RETRY_DELAY_MS = 120;
+const MERMAID_ASPECT_MAX_RETRIES = 3;
+
+type PierreThemeConfig = {
+ theme: { light: string; dark: string };
+ themeType: 'light' | 'dark';
+};
+
+const TOOL_DIFF_UNSAFE_CSS = `
+ [data-diff-header],
+ [data-diff] {
+ [data-separator] {
+ height: 24px !important;
+ }
+ }
+`;
+
+const TOOL_DIFF_METRICS = {
+ hunkLineCount: 50,
+ lineHeight: 24,
+ diffHeaderHeight: 44,
+ hunkSeparatorHeight: 24,
+ fileGap: 0,
+};
+
+const usePierreThemeConfig = (): PierreThemeConfig => {
+ const themeSystem = useOptionalThemeSystem();
+ const fallbackLightTheme = React.useMemo(() => getDefaultTheme(false), []);
+ const fallbackDarkTheme = React.useMemo(() => getDefaultTheme(true), []);
+
+ const availableThemes = React.useMemo(
+ () => themeSystem?.availableThemes ?? [fallbackLightTheme, fallbackDarkTheme],
+ [fallbackDarkTheme, fallbackLightTheme, themeSystem?.availableThemes],
+ );
+ const lightThemeId = themeSystem?.lightThemeId ?? fallbackLightTheme.metadata.id;
+ const darkThemeId = themeSystem?.darkThemeId ?? fallbackDarkTheme.metadata.id;
+
+ const lightTheme = React.useMemo(
+ () => availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? fallbackLightTheme,
+ [availableThemes, fallbackLightTheme, lightThemeId],
+ );
+ const darkTheme = React.useMemo(
+ () => availableThemes.find((theme) => theme.metadata.id === darkThemeId) ?? fallbackDarkTheme,
+ [availableThemes, darkThemeId, fallbackDarkTheme],
+ );
+
+ React.useEffect(() => {
+ ensurePierreThemeRegistered(lightTheme);
+ ensurePierreThemeRegistered(darkTheme);
+ }, [darkTheme, lightTheme]);
+
+ const currentVariant = themeSystem?.currentTheme.metadata.variant ?? 'light';
+
+ return {
+ theme: { light: lightTheme.metadata.id, dark: darkTheme.metadata.id },
+ themeType: currentVariant === 'dark' ? 'dark' : 'light',
+ };
+};
+
+type ViewportSize = { width: number; height: number };
+
+const getWindowViewport = (): ViewportSize => ({
+ width: typeof window !== 'undefined' ? window.innerWidth : 0,
+ height: typeof window !== 'undefined' ? window.innerHeight : 0,
+});
+
+const PREVIEW_VIEWPORT_LIMITS = {
+ mobile: { widthRatio: 0.94, heightRatio: 0.86, padding: 10 },
+ desktop: { widthRatio: 0.8, heightRatio: 0.8, padding: 16 },
+} as const;
+
+const getPreviewViewportBounds = (viewport: { width: number; height: number }, isMobile: boolean) => {
+ const limits = isMobile ? PREVIEW_VIEWPORT_LIMITS.mobile : PREVIEW_VIEWPORT_LIMITS.desktop;
+ const paddedWidth = Math.max(160, viewport.width - limits.padding * 2);
+ const paddedHeight = Math.max(160, viewport.height - limits.padding * 2);
+
+ return {
+ maxWidth: Math.max(160, Math.min(paddedWidth, viewport.width * limits.widthRatio)),
+ maxHeight: Math.max(160, Math.min(paddedHeight, viewport.height * limits.heightRatio)),
+ };
+};
+
+const getSvgAspectRatio = (svg: SVGElement): number | null => {
+ try {
+ const groups = Array.from(svg.querySelectorAll('g'));
+ let bestArea = 0;
+ let bestRatio: number | null = null;
+
+ for (const group of groups) {
+ if (!(group instanceof SVGGraphicsElement)) {
+ continue;
+ }
+ const box = group.getBBox();
+ if (!(box.width > 0 && box.height > 0)) {
+ continue;
+ }
+ const area = box.width * box.height;
+ if (area > bestArea) {
+ bestArea = area;
+ bestRatio = box.width / box.height;
+ }
+ }
+
+ if (bestRatio && Number.isFinite(bestRatio) && bestRatio > 0) {
+ return bestRatio;
+ }
+ } catch {
+ // Ignore getBBox failures and fall back to SVG attrs/viewBox.
+ }
+
+ const viewBox = svg.getAttribute('viewBox');
+ if (viewBox) {
+ const parts = viewBox.trim().split(/\s+/).map(Number);
+ if (parts.length === 4) {
+ const width = parts[2];
+ const height = parts[3];
+ if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
+ return width / height;
+ }
+ }
+ }
+
+ const attrWidth = Number(svg.getAttribute('width'));
+ const attrHeight = Number(svg.getAttribute('height'));
+ if (Number.isFinite(attrWidth) && Number.isFinite(attrHeight) && attrWidth > 0 && attrHeight > 0) {
+ return attrWidth / attrHeight;
+ }
+
+ const rect = svg.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) {
+ return rect.width / rect.height;
+ }
+
+ return null;
+};
+
+const usePreviewOverlayState = (open: boolean) => {
+ const [isRendered, setIsRendered] = React.useState(open);
+ const [isVisible, setIsVisible] = React.useState(open);
+ const [isTransitioning, setIsTransitioning] = React.useState(false);
+
+ React.useEffect(() => {
+ if (open) {
+ setIsRendered(true);
+ setIsTransitioning(true);
+ if (typeof window === 'undefined') {
+ setIsVisible(true);
+ return;
+ }
+
+ const raf = window.requestAnimationFrame(() => {
+ setIsVisible(true);
+ });
+
+ const doneId = window.setTimeout(() => {
+ setIsTransitioning(false);
+ }, PREVIEW_ANIMATION_MS);
+
+ return () => {
+ window.cancelAnimationFrame(raf);
+ window.clearTimeout(doneId);
+ };
+ }
+
+ setIsVisible(false);
+ setIsTransitioning(true);
+ if (typeof window === 'undefined') {
+ setIsRendered(false);
+ return;
+ }
+
+ const timeoutId = window.setTimeout(() => {
+ setIsRendered(false);
+ setIsTransitioning(false);
+ }, PREVIEW_ANIMATION_MS);
+
+ return () => {
+ window.clearTimeout(timeoutId);
+ };
+ }, [open]);
+
+ return { isRendered, isVisible, isTransitioning };
+};
+
+const usePreviewViewport = (open: boolean) => {
+ const [viewport, setViewport] = React.useState(getWindowViewport);
+
+ React.useEffect(() => {
+ if (!open || typeof window === 'undefined') {
+ return;
+ }
+
+ const onResize = () => {
+ setViewport(getWindowViewport());
+ };
+
+ onResize();
+ window.addEventListener('resize', onResize);
+ return () => {
+ window.removeEventListener('resize', onResize);
+ };
+ }, [open]);
+
+ return viewport;
+};
+
+const ImagePreviewDialog: React.FC<{
+ popup: ToolPopupContent;
+ onOpenChange: (open: boolean) => void;
+ isMobile: boolean;
+}> = ({ popup, onOpenChange, isMobile }) => {
+ const gallery = React.useMemo(() => {
+ const baseImage = popup.image;
+ if (!baseImage) return [] as Array<{ url: string; mimeType?: string; filename?: string; size?: number }>;
+ const fromPopup = Array.isArray(baseImage.gallery)
+ ? baseImage.gallery.filter((item): item is { url: string; mimeType?: string; filename?: string; size?: number } => Boolean(item?.url))
+ : [];
+
+ if (fromPopup.length > 0) {
+ return fromPopup;
+ }
+
+ return [{
+ url: baseImage.url,
+ mimeType: baseImage.mimeType,
+ filename: baseImage.filename,
+ size: baseImage.size,
+ }];
+ }, [popup.image]);
+
+ const [currentIndex, setCurrentIndex] = React.useState(0);
+ const [imageNaturalSize, setImageNaturalSize] = React.useState<{ width: number; height: number } | null>(null);
+ const { isRendered, isVisible, isTransitioning } = usePreviewOverlayState(popup.open);
+ const viewport = usePreviewViewport(popup.open);
+
+ React.useEffect(() => {
+ if (!popup.open || gallery.length === 0) {
+ return;
+ }
+
+ const requestedIndex = typeof popup.image?.index === 'number' ? popup.image.index : -1;
+ if (requestedIndex >= 0 && requestedIndex < gallery.length) {
+ setCurrentIndex(requestedIndex);
+ return;
+ }
+
+ const matchingIndex = popup.image?.url
+ ? gallery.findIndex((item) => item.url === popup.image?.url)
+ : -1;
+ setCurrentIndex(matchingIndex >= 0 ? matchingIndex : 0);
+ }, [gallery, popup.image?.index, popup.image?.url, popup.open]);
+
+ const currentImage = gallery[currentIndex] ?? gallery[0] ?? popup.image;
+ const imageTitle = currentImage?.filename || popup.title || 'Image preview';
+ const hasMultipleImages = gallery.length > 1;
+
+ const showPrevious = React.useCallback(() => {
+ if (gallery.length <= 1) return;
+ setCurrentIndex((prev) => (prev - 1 + gallery.length) % gallery.length);
+ }, [gallery.length]);
+
+ const showNext = React.useCallback(() => {
+ if (gallery.length <= 1) return;
+ setCurrentIndex((prev) => (prev + 1) % gallery.length);
+ }, [gallery.length]);
+
+ React.useEffect(() => {
+ if (!popup.open) {
+ return;
+ }
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ onOpenChange(false);
+ return;
+ }
+
+ if (event.key === 'ArrowLeft' && hasMultipleImages) {
+ event.preventDefault();
+ showPrevious();
+ return;
+ }
+
+ if (event.key === 'ArrowRight' && hasMultipleImages) {
+ event.preventDefault();
+ showNext();
+ }
+ };
+
+ window.addEventListener('keydown', onKeyDown);
+ return () => {
+ window.removeEventListener('keydown', onKeyDown);
+ };
+ }, [hasMultipleImages, onOpenChange, popup.open, showNext, showPrevious]);
+
+ React.useEffect(() => {
+ setImageNaturalSize(null);
+ }, [currentImage?.url]);
+
+ const imageDisplaySize = React.useMemo(() => {
+ const maxWidth = Math.max(160, viewport.width * (isMobile ? 0.86 : 0.75));
+ const maxHeight = Math.max(160, viewport.height * (isMobile ? 0.72 : 0.75));
+
+ if (!imageNaturalSize) {
+ return {
+ width: Math.round(maxWidth),
+ height: Math.round(maxHeight),
+ };
+ }
+
+ const widthScale = maxWidth / imageNaturalSize.width;
+ const heightScale = maxHeight / imageNaturalSize.height;
+ const scale = Math.min(widthScale, heightScale);
+
+ return {
+ width: Math.max(1, Math.round(imageNaturalSize.width * scale)),
+ height: Math.max(1, Math.round(imageNaturalSize.height * scale)),
+ };
+ }, [imageNaturalSize, isMobile, viewport.height, viewport.width]);
+
+ if (!isRendered || !currentImage || typeof document === 'undefined') {
+ return null;
+ }
+
+ const content = (
+
+
onOpenChange(false)}
+ />
+
+ {hasMultipleImages && (
+ <>
+
event.stopPropagation()}
+ onClick={showPrevious}
+ className="absolute left-3 top-1/2 -translate-y-1/2 z-10 h-10 w-10 flex items-center justify-center rounded-full bg-black/25 text-foreground/90 backdrop-blur-sm hover:bg-black/35 focus:outline-none focus:ring-2 focus:ring-primary/60"
+ aria-label="Previous image"
+ >
+
+
+
event.stopPropagation()}
+ onClick={showNext}
+ className="absolute right-3 top-1/2 -translate-y-1/2 z-10 h-10 w-10 flex items-center justify-center rounded-full bg-black/25 text-foreground/90 backdrop-blur-sm hover:bg-black/35 focus:outline-none focus:ring-2 focus:ring-primary/60"
+ aria-label="Next image"
+ >
+
+
+ >
+ )}
+
+
+
+
+
+ {imageTitle}
+
+
onOpenChange(false)}
+ aria-label="Close image preview"
+ >
+
+
+
+
+
{
+ const element = event.currentTarget;
+ const width = element.naturalWidth;
+ const height = element.naturalHeight;
+ if (width > 0 && height > 0) {
+ setImageNaturalSize((previous) => {
+ if (previous && previous.width === width && previous.height === height) {
+ return previous;
+ }
+ return { width, height };
+ });
+ }
+ }}
+ />
+
+
+
+ );
+
+ return createPortal(content, document.body);
+};
+
+// ── PERF-007: Virtualised sub-components for dialog ──────────────────
+
+const DialogUnifiedDiff: React.FC<{
+ popup: ToolPopupContent;
+ diffViewMode: DiffViewMode;
+ pierreThemeConfig: PierreThemeConfig;
+}> = React.memo(({ popup, diffViewMode, pierreThemeConfig }) => {
+ const patchContent = popup.content || '';
+
+ return (
+
+ );
+});
+
+DialogUnifiedDiff.displayName = 'DialogUnifiedDiff';
+
+const DialogReadContent: React.FC<{
+ popup: ToolPopupContent;
+ syntaxTheme: Record
;
+ pierreThemeConfig: PierreThemeConfig;
+}> = React.memo(({ popup, syntaxTheme, pierreThemeConfig }) => {
+ const parsedReadOutput = React.useMemo(() => parseReadToolOutput(popup.content), [popup.content]);
+
+ const inputMeta = popup.metadata?.input;
+ const inputObj = typeof inputMeta === 'object' && inputMeta !== null ? (inputMeta as Record) : {};
+ const offset = typeof inputObj.offset === 'number' ? inputObj.offset : 0;
+ const filePath =
+ typeof inputObj.file_path === 'string'
+ ? inputObj.file_path
+ : typeof inputObj.filePath === 'string'
+ ? inputObj.filePath
+ : typeof inputObj.path === 'string'
+ ? inputObj.path
+ : 'read-output';
+
+ const fileContents = React.useMemo(() => parsedReadOutput.lines.map((line) => line.text).join('\n'), [parsedReadOutput]);
+ const detectedLanguage = React.useMemo(
+ () => popup.language || getLanguageFromExtension(filePath) || 'text',
+ [filePath, popup.language],
+ );
+
+ const codeLines: CodeLine[] = React.useMemo(() => {
+ const hasExplicitLineNumbers = parsedReadOutput.lines.some((line) => line.lineNumber !== null);
+ const result: CodeLine[] = [];
+ let nextLineNumber = offset;
+
+ for (const line of parsedReadOutput.lines) {
+ if (line.lineNumber !== null) {
+ nextLineNumber = line.lineNumber;
+ }
+ const shouldAssignFallback =
+ parsedReadOutput.type === 'file'
+ && !hasExplicitLineNumbers
+ && line.lineNumber === null
+ && !line.isInfo;
+ const effectiveLineNumber = line.lineNumber ?? (shouldAssignFallback
+ ? (nextLineNumber + 1)
+ : null);
+ if (typeof effectiveLineNumber === 'number') {
+ nextLineNumber = effectiveLineNumber;
+ }
+
+ result.push({
+ text: line.text,
+ lineNumber: effectiveLineNumber,
+ isInfo: line.isInfo,
+ });
+ }
+
+ return result;
+ }, [offset, parsedReadOutput]);
+
+ if (parsedReadOutput.type === 'file') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+});
+
+DialogReadContent.displayName = 'DialogReadContent';
+const MermaidPreviewDialog: React.FC<{
+ popup: ToolPopupContent;
+ onOpenChange: (open: boolean) => void;
+ isMobile: boolean;
+}> = ({ popup, onOpenChange, isMobile }) => {
+ const [source, setSource] = React.useState(popup.mermaid?.source || '');
+ const [status, setStatus] = React.useState<'idle' | 'loading' | 'ready' | 'error'>(popup.mermaid?.source ? 'ready' : 'idle');
+ const [errorMessage, setErrorMessage] = React.useState('');
+ const { isRendered, isVisible, isTransitioning } = usePreviewOverlayState(popup.open);
+ const [diagramAspectRatio, setDiagramAspectRatio] = React.useState(null);
+ const viewport = usePreviewViewport(popup.open);
+ const requestIdRef = React.useRef(0);
+ const mermaidPreviewRef = React.useRef(null);
+
+ const normalizeFilePath = React.useCallback((rawPath: string): string | null => {
+ const input = rawPath.trim();
+ if (!input.toLowerCase().startsWith('file://')) {
+ return null;
+ }
+
+ const isSafeLocalPath = (path: string): boolean => {
+ if (!path || /[\0\r\n]/.test(path)) {
+ return false;
+ }
+
+ const normalized = path.replace(/\\/g, '/');
+ const segments = normalized.split('/').filter(Boolean);
+ if (segments.includes('..')) {
+ return false;
+ }
+
+ if (normalized.startsWith('/')) {
+ return true;
+ }
+
+ return /^[A-Za-z]:\//.test(normalized);
+ };
+
+ const decodeLoose = (value: string): string => {
+ return value.replace(/%([0-9A-Fa-f]{2})/g, (_match, hex: string) => {
+ const codePoint = Number.parseInt(hex, 16);
+ return Number.isFinite(codePoint) ? String.fromCharCode(codePoint) : `%${hex}`;
+ });
+ };
+
+ const canParse = typeof URL.canParse === 'function'
+ ? URL.canParse(input)
+ : false;
+
+ if (canParse) {
+ let pathname = decodeLoose(new URL(input).pathname || '');
+ if (/^\/[A-Za-z]:\//.test(pathname)) {
+ pathname = pathname.slice(1);
+ }
+ return isSafeLocalPath(pathname) ? pathname : null;
+ }
+
+ const stripped = input.replace(/^file:\/\//i, '');
+ const decoded = decodeLoose(stripped);
+ return isSafeLocalPath(decoded) ? decoded : (isSafeLocalPath(stripped) ? stripped : null);
+ }, []);
+
+ const decodeDataUrl = React.useCallback((value: string): string => {
+ const commaIndex = value.indexOf(',');
+ if (commaIndex < 0) {
+ throw new Error('Malformed data URL');
+ }
+
+ const metadata = value.slice(0, commaIndex).toLowerCase();
+ const payload = value.slice(commaIndex + 1);
+ if (metadata.includes(';base64')) {
+ return atob(payload);
+ }
+ return decodeURIComponent(payload);
+ }, []);
+
+ const loadMermaidSource = React.useCallback(async () => {
+ const target = popup.mermaid;
+ if (!target?.url) {
+ setStatus('error');
+ setErrorMessage('Missing Mermaid source URL.');
+ return;
+ }
+
+ if (target.source) {
+ setSource(target.source);
+ setStatus('ready');
+ setErrorMessage('');
+ return;
+ }
+
+ const requestId = requestIdRef.current + 1;
+ requestIdRef.current = requestId;
+
+ setStatus('loading');
+ setErrorMessage('');
+
+ let sourcePromise: Promise;
+ if (target.url.startsWith('data:')) {
+ sourcePromise = Promise.resolve(decodeDataUrl(target.url));
+ } else if (target.url.toLowerCase().startsWith('file://')) {
+ const normalizedPath = normalizeFilePath(target.url);
+ if (!normalizedPath) {
+ sourcePromise = Promise.reject(new Error('Invalid local file path for Mermaid preview.'));
+ } else {
+ sourcePromise = fetch(`/api/fs/raw?path=${encodeURIComponent(normalizedPath)}`)
+ .then((response) => {
+ if (!response.ok) {
+ return Promise.reject(new Error(`Failed to read diagram file (${response.status})`));
+ }
+ return response.text();
+ });
+ }
+ } else {
+ const canParse = typeof URL.canParse === 'function'
+ ? URL.canParse(target.url, window.location.origin)
+ : false;
+ const resolvedUrl = canParse ? new URL(target.url, window.location.origin) : null;
+
+ if (!resolvedUrl || (resolvedUrl.protocol !== 'http:' && resolvedUrl.protocol !== 'https:')) {
+ sourcePromise = Promise.reject(new Error('Unsupported Mermaid URL protocol.'));
+ } else {
+ sourcePromise = fetch(resolvedUrl.toString())
+ .then((response) => {
+ if (!response.ok) {
+ return Promise.reject(new Error(`Failed to load diagram (${response.status})`));
+ }
+ return response.text();
+ });
+ }
+ }
+
+ await sourcePromise
+ .then((resolvedSource) => {
+ if (requestIdRef.current !== requestId) {
+ return;
+ }
+
+ setSource(resolvedSource);
+ setStatus('ready');
+ })
+ .catch((error) => {
+ if (requestIdRef.current !== requestId) {
+ return;
+ }
+ setStatus('error');
+ setErrorMessage(error instanceof Error ? error.message : 'Unable to load Mermaid diagram.');
+ });
+ }, [decodeDataUrl, normalizeFilePath, popup.mermaid]);
+
+ React.useEffect(() => {
+ if (!popup.open || !popup.mermaid) {
+ return;
+ }
+ void loadMermaidSource();
+ }, [loadMermaidSource, popup.mermaid, popup.open]);
+
+ React.useEffect(() => {
+ if (!popup.open) {
+ return;
+ }
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ onOpenChange(false);
+ }
+ };
+
+ window.addEventListener('keydown', onKeyDown);
+ return () => {
+ window.removeEventListener('keydown', onKeyDown);
+ };
+ }, [onOpenChange, popup.open]);
+
+ React.useEffect(() => {
+ if (!popup.open || status !== 'ready') {
+ setDiagramAspectRatio(null);
+ return;
+ }
+
+ const measureAspectRatio = () => {
+ const svg = mermaidPreviewRef.current?.querySelector('svg');
+ if (!svg) {
+ return false;
+ }
+
+ const aspectRatio = getSvgAspectRatio(svg as SVGElement);
+ if (!aspectRatio || !Number.isFinite(aspectRatio) || aspectRatio <= 0) {
+ return false;
+ }
+
+ setDiagramAspectRatio((previous) => {
+ if (previous && Math.abs(previous - aspectRatio) < 0.001) {
+ return previous;
+ }
+ return aspectRatio;
+ });
+ return true;
+ };
+
+ let rafId = window.requestAnimationFrame(() => {
+ if (!measureAspectRatio()) {
+ rafId = window.requestAnimationFrame(() => {
+ measureAspectRatio();
+ });
+ }
+ });
+
+ let retryCount = 0;
+ let timeoutId: number | undefined;
+ const scheduleRetry = () => {
+ if (retryCount >= MERMAID_ASPECT_MAX_RETRIES) {
+ return;
+ }
+
+ timeoutId = window.setTimeout(() => {
+ retryCount += 1;
+ if (!measureAspectRatio()) {
+ scheduleRetry();
+ }
+ }, MERMAID_ASPECT_RETRY_DELAY_MS);
+ };
+ scheduleRetry();
+
+ const observer = new MutationObserver(() => {
+ measureAspectRatio();
+ });
+
+ if (mermaidPreviewRef.current) {
+ observer.observe(mermaidPreviewRef.current, { childList: true, subtree: true, attributes: true });
+ }
+
+ return () => {
+ window.cancelAnimationFrame(rafId);
+ if (typeof timeoutId === 'number') {
+ window.clearTimeout(timeoutId);
+ }
+ observer.disconnect();
+ };
+ }, [popup.open, source, status]);
+
+ const mermaidMarkdown = `\`\`\`mermaid\n${source}\n\`\`\``;
+
+ const dialogSize = React.useMemo(() => {
+ const { maxWidth, maxHeight } = getPreviewViewportBounds(viewport, isMobile);
+ const availableDiagramHeight = Math.max(160, maxHeight - MERMAID_DIALOG_HEADER_HEIGHT);
+
+ if (diagramAspectRatio && diagramAspectRatio < 1) {
+ const squareSide = Math.min(maxWidth, availableDiagramHeight);
+ return { width: Math.round(squareSide), height: Math.round(squareSide) };
+ }
+
+ return { width: Math.round(maxWidth), height: Math.round(availableDiagramHeight) };
+ }, [diagramAspectRatio, isMobile, viewport]);
+
+ if (!isRendered || typeof document === 'undefined') {
+ return null;
+ }
+
+ const content = (
+
+
onOpenChange(false)}
+ />
+
+
+
event.stopPropagation()}
+ >
+
+ onOpenChange(false)}
+ aria-label="Close diagram preview"
+ >
+
+
+
+
+
+ {status === 'loading' && (
+
+
+ Loading diagram...
+
+ )}
+
+ {status === 'error' && (
+
+
+ {errorMessage || 'Unable to render Mermaid diagram.'}
+
+
{
+ void loadMermaidSource();
+ }}
+ className="px-3 py-1.5 rounded-lg typography-meta border transition-colors hover:bg-[var(--interactive-hover)]"
+ style={{
+ borderColor: 'var(--interactive-border)',
+ color: 'var(--surface-foreground)',
+ }}
+ >
+ Retry
+
+
+ )}
+
+ {status === 'ready' && (
+
+
+
+ )}
+
+
+
+
+
+ );
+
+ return createPortal(content, document.body);
+};
+
+const ToolOutputDialog: React.FC
= ({ popup, onOpenChange, syntaxTheme, isMobile }) => {
+ const [diffViewMode, setDiffViewMode] = React.useState('unified');
+ const pierreThemeConfig = usePierreThemeConfig();
+
+ React.useEffect(() => {
+ if (!popup.open) return;
+ setDiffViewMode('unified');
+ }, [popup.open, popup.title]);
+
+ if (popup.image) {
+ return ;
+ }
+
+ if (popup.mermaid) {
+ return ;
+ }
+
+ return (
+
+ button]:top-1.5',
+ isMobile ? 'w-[95vw] max-w-[95vw]' : 'max-w-5xl',
+ isMobile ? '[&>button]:right-1' : '[&>button]:top-2.5 [&>button]:right-4'
+ )}
+ style={{ maxHeight: '90vh' }}
+ >
+
+
+ {popup.metadata?.tool ? getToolIcon(popup.metadata.tool as string) : (
+
+ )}
+ {popup.title}
+ {popup.isDiff && (
+
+ )}
+
+
+
+
+ {popup.metadata?.input && typeof popup.metadata.input === 'object' &&
+ Object.keys(popup.metadata.input).length > 0 &&
+ popup.metadata?.tool !== 'todowrite' &&
+ popup.metadata?.tool !== 'todoread' &&
+ popup.metadata?.tool !== 'apply_patch' ? (() => {
+ const meta = popup.metadata!;
+ const input = meta.input as Record
;
+
+ const getInputValue = (key: string): string | null => {
+ const val = input[key];
+ return typeof val === 'string' ? val : (typeof val === 'number' ? String(val) : null);
+ };
+ return (
+
+
+ {meta.tool === 'bash'
+ ? 'Command:'
+ : meta.tool === 'task'
+ ? 'Task Details:'
+ : 'Input:'}
+
+ {meta.tool === 'bash' && getInputValue('command') ? (
+
+
+ {getInputValue('command')!}
+
+
+ ) : meta.tool === 'task' && getInputValue('prompt') ? (
+
+ {getInputValue('description') ? `Task: ${getInputValue('description')}\n` : ''}
+ {getInputValue('subagent_type') ? `Agent Type: ${getInputValue('subagent_type')}\n` : ''}
+ {`Instructions:\n${getInputValue('prompt')}`}
+
+ ) : meta.tool === 'write' && getInputValue('content') ? (
+
+ ) : (
+
+ {formatInputForDisplay(input, meta.tool as string)}
+
+ )}
+
+ );
+ })() : null}
+
+ {popup.isDiff ? (
+
+ ) : popup.content ? (
+
+ {(() => {
+ const tool = popup.metadata?.tool;
+
+ if (tool === 'todowrite' || tool === 'todoread') {
+ return (
+ renderTodoOutput(popup.content) || (
+
+ {popup.content}
+
+ )
+ );
+ }
+
+ if (tool === 'list') {
+ return (
+ renderListOutput(popup.content) || (
+
+ {popup.content}
+
+ )
+ );
+ }
+
+ if (tool === 'grep') {
+ return (
+ renderGrepOutput(popup.content, isMobile) || (
+
+ {popup.content}
+
+ )
+ );
+ }
+
+ if (tool === 'glob') {
+ return (
+ renderGlobOutput(popup.content, isMobile) || (
+
+ {popup.content}
+
+ )
+ );
+ }
+
+ if (tool === 'task' || tool === 'reasoning') {
+ return (
+
+
+
+ );
+ }
+
+ if (tool === 'web-search' || tool === 'websearch' || tool === 'search_web') {
+ return (
+ renderWebSearchOutput(popup.content, syntaxTheme) || (
+
+ {popup.content}
+
+ )
+ );
+ }
+
+ if (tool === 'read') {
+ return
;
+ }
+
+ return (
+
+ {popup.content}
+
+ );
+ })()}
+
+ ) : (
+
+
Command completed successfully
+
No output was produced
+
+ )}
+
+
+
+
+ );
+};
+
+export default ToolOutputDialog;
diff --git a/ui/src/components/chat/message/messageRole.ts b/ui/src/components/chat/message/messageRole.ts
new file mode 100644
index 0000000..b0c42b9
--- /dev/null
+++ b/ui/src/components/chat/message/messageRole.ts
@@ -0,0 +1,34 @@
+import type { Message } from '@opencode-ai/sdk/v2';
+
+export interface MessageRoleInfo {
+ role: string;
+ isUser: boolean;
+}
+
+export const deriveMessageRole = (
+ messageInfo: Message | (Message & { clientRole?: string; userMessageMarker?: boolean })
+): MessageRoleInfo => {
+ const info = messageInfo as Message & { clientRole?: string; userMessageMarker?: boolean; origin?: string; source?: string };
+ const clientRole = info?.clientRole;
+ const serverRole = info?.role;
+ const userMarker = info?.userMessageMarker === true;
+
+ const isUser =
+ userMarker ||
+ clientRole === 'user' ||
+ serverRole === 'user' ||
+ info?.origin === 'user' ||
+ info?.source === 'user';
+
+ if (isUser) {
+ return {
+ role: 'user',
+ isUser: true,
+ };
+ }
+
+ return {
+ role: clientRole || serverRole || 'assistant',
+ isUser: false,
+ };
+};
diff --git a/ui/src/components/chat/message/partUtils.ts b/ui/src/components/chat/message/partUtils.ts
new file mode 100644
index 0000000..7a55767
--- /dev/null
+++ b/ui/src/components/chat/message/partUtils.ts
@@ -0,0 +1,70 @@
+import type { Part } from '@opencode-ai/sdk/v2';
+
+type PartWithText = Part & { text?: string; content?: string; value?: string };
+
+export const extractTextContent = (part: Part): string => {
+ const partWithText = part as PartWithText;
+ const rawText = partWithText.text;
+ if (typeof rawText === 'string') {
+ return rawText;
+ }
+ return partWithText.content || partWithText.value || '';
+};
+
+export const isEmptyTextPart = (part: Part): boolean => {
+ if (part.type !== 'text') {
+ return false;
+ }
+ const text = extractTextContent(part);
+ return !text || text.trim().length === 0;
+};
+
+type PartWithSynthetic = Part & { synthetic?: boolean };
+
+interface VisibleFilterOptions {
+ includeReasoning?: boolean;
+}
+
+export const filterVisibleParts = (parts: Part[], options: VisibleFilterOptions = {}): Part[] => {
+ const { includeReasoning = true } = options;
+
+ // Check if there are any non-synthetic parts
+ const hasNonSynthetic = parts.some((part) => {
+ const partWithSynthetic = part as PartWithSynthetic;
+ return !partWithSynthetic.synthetic;
+ });
+
+ return parts.filter((part) => {
+ const partWithSynthetic = part as PartWithSynthetic;
+ const isSynthetic = Boolean(partWithSynthetic.synthetic);
+
+ if (isSynthetic && part.type === 'text') {
+ const text = extractTextContent(part);
+ if (text.includes('')) {
+ return false;
+ }
+ }
+
+ // Only filter out synthetic parts if there are non-synthetic parts present
+ // Otherwise, show synthetic parts so the message is displayed
+ if (isSynthetic && hasNonSynthetic) {
+ return false;
+ }
+ if (!includeReasoning && part.type === 'reasoning') {
+ return false;
+ }
+ const isPatchPart = part.type === 'patch';
+
+ return !isPatchPart;
+ });
+};
+
+type PartWithTime = Part & { time?: { start?: number; end?: number } };
+
+export const isFinalizedTextPart = (part: Part): boolean => {
+ if (part.type !== 'text') {
+ return false;
+ }
+ const time = (part as PartWithTime).time;
+ return Boolean(time && typeof time.end !== 'undefined');
+};
diff --git a/ui/src/components/chat/message/parts/AssistantTextPart.tsx b/ui/src/components/chat/message/parts/AssistantTextPart.tsx
new file mode 100644
index 0000000..d589007
--- /dev/null
+++ b/ui/src/components/chat/message/parts/AssistantTextPart.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import type { Part } from '@opencode-ai/sdk/v2';
+import { MarkdownRenderer } from '../../MarkdownRenderer';
+import type { StreamPhase } from '../types';
+import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
+import { ReasoningTimelineBlock, formatReasoningText } from './ReasoningPart';
+
+type PartWithText = Part & { text?: string; content?: string; value?: string; time?: { start?: number; end?: number } };
+
+interface AssistantTextPartProps {
+ part: Part;
+ messageId: string;
+ streamPhase: StreamPhase;
+ allowAnimation: boolean;
+ onContentChange?: (reason?: ContentChangeReason, messageId?: string) => void;
+ renderAsReasoning?: boolean;
+}
+
+const AssistantTextPart: React.FC = ({
+ part,
+ messageId,
+ streamPhase,
+ allowAnimation,
+ onContentChange,
+ renderAsReasoning = false,
+}) => {
+ const partWithText = part as PartWithText;
+ const rawText = partWithText.text;
+ const baseTextContent = typeof rawText === 'string' ? rawText : partWithText.content || partWithText.value || '';
+ const textContent = React.useMemo(() => {
+ if (renderAsReasoning) {
+ return formatReasoningText(baseTextContent);
+ }
+ return baseTextContent;
+ }, [baseTextContent, renderAsReasoning]);
+ const isStreamingPhase = streamPhase === 'streaming';
+ const isCooldownPhase = streamPhase === 'cooldown';
+ const wasStreamingRef = React.useRef(isStreamingPhase);
+
+ if (isStreamingPhase || isCooldownPhase) {
+ wasStreamingRef.current = true;
+ return null;
+ }
+
+ const time = partWithText.time;
+ const isFinalized = time && typeof time.end !== 'undefined';
+
+ if (!isFinalized && (!textContent || textContent.trim().length === 0)) {
+ return null;
+ }
+
+ if (!textContent || textContent.trim().length === 0) {
+ return null;
+ }
+
+ if (renderAsReasoning) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default AssistantTextPart;
diff --git a/ui/src/components/chat/message/parts/JustificationBlock.tsx b/ui/src/components/chat/message/parts/JustificationBlock.tsx
new file mode 100644
index 0000000..9d34051
--- /dev/null
+++ b/ui/src/components/chat/message/parts/JustificationBlock.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import type { Part } from '@opencode-ai/sdk/v2';
+import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
+import { ReasoningTimelineBlock } from './ReasoningPart';
+
+type PartWithText = Part & { text?: string; content?: string; time?: { start?: number; end?: number } };
+
+const cleanJustificationText = (text: string): string => {
+ if (typeof text !== 'string' || text.trim().length === 0) {
+ return '';
+ }
+
+ return text
+ .split('\n')
+ .map((line: string) => line.replace(/^>\s?/, '').trimEnd())
+ .filter((line: string) => line.trim().length > 0)
+ .join('\n')
+ .trim();
+};
+
+interface JustificationBlockProps {
+ part: Part;
+ messageId: string;
+ onContentChange?: (reason?: ContentChangeReason) => void;
+}
+
+const JustificationBlock: React.FC = ({
+ part,
+ messageId,
+ onContentChange,
+}) => {
+ const partWithText = part as PartWithText;
+ const rawText = partWithText.text || partWithText.content || '';
+ const textContent = React.useMemo(() => cleanJustificationText(rawText), [rawText]);
+ const time = partWithText.time;
+
+ // Don't render if there's no text content
+ if (!textContent || textContent.trim().length === 0) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+export default React.memo(JustificationBlock);
diff --git a/ui/src/components/chat/message/parts/MigratingPart.tsx b/ui/src/components/chat/message/parts/MigratingPart.tsx
new file mode 100644
index 0000000..54f71d3
--- /dev/null
+++ b/ui/src/components/chat/message/parts/MigratingPart.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+
+interface MigratingPartProps {
+
+ isMigrating: boolean;
+ children: React.ReactNode;
+ className?: string;
+}
+
+const MigratingPart: React.FC = ({
+ isMigrating,
+ children,
+ className,
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default React.memo(MigratingPart);
diff --git a/ui/src/components/chat/message/parts/ProgressiveGroup.tsx b/ui/src/components/chat/message/parts/ProgressiveGroup.tsx
new file mode 100644
index 0000000..8a0f49f
--- /dev/null
+++ b/ui/src/components/chat/message/parts/ProgressiveGroup.tsx
@@ -0,0 +1,322 @@
+import React from 'react';
+import { RiArrowDownSLine, RiArrowRightSLine, RiStackLine } from '@remixicon/react';
+import { cn } from '@/lib/utils';
+import type { TurnActivityPart } from '../../hooks/useTurnGrouping';
+import type { ToolPart as ToolPartType } from '@opencode-ai/sdk/v2';
+import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
+import type { ToolPopupContent } from '../types';
+import ToolPart from './ToolPart';
+import ReasoningPart from './ReasoningPart';
+import JustificationBlock from './JustificationBlock';
+import { FadeInOnReveal } from '../FadeInOnReveal';
+
+const MAX_VISIBLE_COLLAPSED = 6;
+
+interface DiffStats {
+ additions: number;
+ deletions: number;
+ files: number;
+}
+
+interface ProgressiveGroupProps {
+ parts: TurnActivityPart[];
+ isExpanded: boolean;
+ onToggle: () => void;
+ syntaxTheme: Record;
+ isMobile: boolean;
+ expandedTools: Set;
+ onToggleTool: (toolId: string) => void;
+ onShowPopup: (content: ToolPopupContent) => void;
+ onContentChange?: (reason?: ContentChangeReason) => void;
+ diffStats?: DiffStats;
+}
+
+const sortPartsByTime = (parts: TurnActivityPart[]): TurnActivityPart[] => {
+ return [...parts].sort((a, b) => {
+ const aTime = typeof a.endedAt === 'number' ? a.endedAt : undefined;
+ const bTime = typeof b.endedAt === 'number' ? b.endedAt : undefined;
+
+ if (aTime === undefined && bTime === undefined) return 0;
+ if (aTime === undefined) return 1;
+ if (bTime === undefined) return -1;
+
+ return aTime - bTime;
+ });
+};
+
+const getToolConnections = (
+ parts: TurnActivityPart[]
+): Record => {
+ const connections: Record = {};
+ const toolParts = parts.filter((p) => p.kind === 'tool');
+
+ toolParts.forEach((activity, index) => {
+ const partId = activity.id;
+ connections[partId] = {
+ hasPrev: index > 0,
+ hasNext: index < toolParts.length - 1,
+ };
+ });
+
+ return connections;
+};
+
+const ProgressiveGroup: React.FC = ({
+ parts,
+ isExpanded,
+ onToggle,
+ syntaxTheme,
+ isMobile,
+ expandedTools,
+ onToggleTool,
+ onShowPopup,
+ onContentChange,
+ diffStats,
+}) => {
+ const previousExpandedRef = React.useRef(isExpanded);
+ // Track if we just expanded from collapsed state
+ const [justExpandedFromCollapsed, setJustExpandedFromCollapsed] = React.useState(false);
+
+ const [expansionKey, setExpansionKey] = React.useState(0);
+
+ // Track which parts have already been shown in collapsed view (for fade-in animation)
+ const shownInCollapsedRef = React.useRef>(new Set());
+
+ React.useEffect(() => {
+ if (previousExpandedRef.current === isExpanded) return;
+ const wasCollapsed = previousExpandedRef.current === false;
+ previousExpandedRef.current = isExpanded;
+ onContentChange?.('structural');
+
+ if (isExpanded && wasCollapsed) {
+ setExpansionKey((k) => k + 1);
+ setJustExpandedFromCollapsed(true);
+ // Clear collapsed tracking when expanding (will restart when collapsed again)
+ shownInCollapsedRef.current.clear();
+ // Reset after a short delay (after animations would have started)
+ const timer = setTimeout(() => setJustExpandedFromCollapsed(false), 50);
+ return () => clearTimeout(timer);
+ } else {
+ setJustExpandedFromCollapsed(false);
+ }
+ }, [isExpanded, onContentChange]);
+
+ const displayParts = React.useMemo(() => {
+ return sortPartsByTime(parts);
+ }, [parts]);
+
+ const toolConnections = getToolConnections(displayParts);
+
+ // For collapsed state: show last N items, but ensure at least one in-flight item is visible if exists
+ const visibleCollapsedParts = React.useMemo(() => {
+ const defaultVisible = displayParts.slice(-MAX_VISIBLE_COLLAPSED);
+
+ const hasVisibleActive = defaultVisible.some((p) => p.endedAt === undefined);
+ if (hasVisibleActive) {
+ return defaultVisible;
+ }
+
+ const activeParts = displayParts.filter((p) => p.endedAt === undefined);
+ if (activeParts.length === 0) {
+ return defaultVisible;
+ }
+
+ const newestActive = activeParts[activeParts.length - 1];
+ const visibleIds = new Set(defaultVisible.map((p) => p.id));
+
+ if (visibleIds.has(newestActive.id)) {
+ return defaultVisible;
+ }
+
+ const replacementIndex = 0;
+ const result = [...defaultVisible];
+ result[replacementIndex] = newestActive;
+
+ return result.sort((a, b) => {
+ const aIndex = displayParts.findIndex((p) => p.id === a.id);
+ const bIndex = displayParts.findIndex((p) => p.id === b.id);
+ return aIndex - bIndex;
+ });
+ }, [displayParts]);
+
+ // Set of part IDs that were visible in collapsed state
+ const visibleInCollapsedIds = React.useMemo(() => {
+ const ids = new Set();
+ visibleCollapsedParts.forEach((p) => {
+ ids.add(p.id);
+ });
+ return ids;
+ }, [visibleCollapsedParts]);
+
+ // Connections for collapsed view (based on visible parts only)
+ const collapsedToolConnections = React.useMemo(() => {
+ return getToolConnections(visibleCollapsedParts);
+ }, [visibleCollapsedParts]);
+
+ const hiddenCount = Math.max(0, displayParts.length - MAX_VISIBLE_COLLAPSED);
+
+ if (displayParts.length === 0) {
+ return null;
+ }
+
+ const partsToRender = isExpanded ? displayParts : visibleCollapsedParts;
+ const connectionsToUse = isExpanded ? toolConnections : collapsedToolConnections;
+
+ // If there are no hidden items, header is not interactive
+ const isHeaderInteractive = hiddenCount > 0;
+
+ return (
+
+
+
+
+
+ {isHeaderInteractive ? (
+ <>
+
+
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ >
+ ) : (
+
+ )}
+
+
Activity
+
+
+ {diffStats && (diffStats.additions > 0 || diffStats.deletions > 0) && (
+
+
+
+ +{Math.max(0, diffStats.additions)}
+
+ /
+
+ -{Math.max(0, diffStats.deletions)}
+
+
+
+ )}
+
+
+
+
+ {!isExpanded && hiddenCount > 0 && (
+
+ +{hiddenCount} more...
+
+ )}
+
+ {partsToRender.map((activity) => {
+ const partId = activity.id;
+ const connection = connectionsToUse[partId];
+
+ const animationKey = `${partId}-exp${expansionKey}`;
+
+ // Determine if animation should be skipped:
+ // 1. When expanding from collapsed: skip for items that were already visible
+ // 2. When collapsed: skip for items already shown before (track in ref)
+ const wasVisibleInCollapsed = visibleInCollapsedIds.has(activity.id);
+
+ let skipAnimation = false;
+ if (justExpandedFromCollapsed && wasVisibleInCollapsed) {
+ // Expanding: don't animate items that were already visible in collapsed state
+ skipAnimation = true;
+ } else if (!isExpanded) {
+ // Collapsed: animate only items that haven't been shown yet
+ if (shownInCollapsedRef.current.has(activity.id)) {
+ skipAnimation = true;
+ } else {
+ // Mark as shown for future renders
+ shownInCollapsedRef.current.add(activity.id);
+ }
+ }
+
+ switch (activity.kind) {
+ case 'tool':
+ return (
+
+ onToggleTool(partId)}
+ syntaxTheme={syntaxTheme}
+ isMobile={isMobile}
+ onContentChange={onContentChange}
+ onShowPopup={onShowPopup}
+ hasPrevTool={connection?.hasPrev ?? false}
+ hasNextTool={connection?.hasNext ?? false}
+ />
+
+ );
+
+ case 'reasoning':
+ return (
+
+
+
+ );
+
+ case 'justification':
+ return (
+
+
+
+ );
+
+ default:
+ return null;
+ }
+ })}
+
+
+
+ );
+};
+
+export default React.memo(ProgressiveGroup);
diff --git a/ui/src/components/chat/message/parts/ReasoningPart.tsx b/ui/src/components/chat/message/parts/ReasoningPart.tsx
new file mode 100644
index 0000000..c232a4c
--- /dev/null
+++ b/ui/src/components/chat/message/parts/ReasoningPart.tsx
@@ -0,0 +1,256 @@
+import React from 'react';
+import type { ComponentType } from 'react';
+import type { Part } from '@opencode-ai/sdk/v2';
+import { RiArrowDownSLine, RiArrowRightSLine, RiBrainAi3Line, RiChatAi3Line } from '@remixicon/react';
+import { cn } from '@/lib/utils';
+import { formatTimestampForDisplay } from '../timeFormat';
+import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
+import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
+import { useUIStore } from '@/stores/useUIStore';
+
+type PartWithText = Part & { text?: string; content?: string; time?: { start?: number; end?: number } };
+
+export type ReasoningVariant = 'thinking' | 'justification';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type IconComponent = ComponentType;
+
+const variantConfig: Record<
+ ReasoningVariant,
+ { label: string; Icon: IconComponent }
+> = {
+ thinking: { label: 'Thinking', Icon: RiBrainAi3Line },
+ justification: { label: 'Justification', Icon: RiChatAi3Line },
+};
+
+const cleanReasoningText = (text: string): string => {
+ if (typeof text !== 'string' || text.trim().length === 0) {
+ return '';
+ }
+
+ return text
+ .split('\n')
+ .map((line: string) => line.replace(/^>\s?/, '').trimEnd())
+ .filter((line: string) => line.trim().length > 0)
+ .join('\n')
+ .trim();
+};
+
+const getReasoningSummary = (text: string): string => {
+ if (!text) {
+ return '';
+ }
+
+ const trimmed = text.trim();
+ const newlineIndex = trimmed.indexOf('\n');
+ const periodIndex = trimmed.indexOf('.');
+
+ const cutoffCandidates = [
+ newlineIndex >= 0 ? newlineIndex : Infinity,
+ periodIndex >= 0 ? periodIndex : Infinity,
+ ];
+ const cutoff = Math.min(...cutoffCandidates);
+
+ if (!Number.isFinite(cutoff)) {
+ return trimmed;
+ }
+
+ return trimmed.substring(0, cutoff).trim();
+};
+
+const formatDuration = (start: number, end?: number, now: number = Date.now()): string => {
+ const duration = end ? end - start : now - start;
+ const seconds = duration / 1000;
+ const displaySeconds = seconds < 0.05 && end !== undefined ? 0.1 : seconds;
+ return `${displaySeconds.toFixed(1)}s`;
+};
+
+const LiveDuration: React.FC<{ start: number; end?: number; active: boolean }> = ({ start, end, active }) => {
+ const [now, setNow] = React.useState(() => Date.now());
+
+ React.useEffect(() => {
+ if (!active) {
+ return;
+ }
+
+ const timer = window.setInterval(() => {
+ setNow(Date.now());
+ }, 100);
+
+ return () => window.clearInterval(timer);
+ }, [active]);
+
+ return <>{formatDuration(start, end, now)}>;
+};
+
+type ReasoningTimelineBlockProps = {
+ text: string;
+ variant: ReasoningVariant;
+ onContentChange?: (reason?: ContentChangeReason) => void;
+ blockId: string;
+ time?: { start?: number; end?: number };
+};
+
+export const ReasoningTimelineBlock: React.FC = ({
+ text,
+ variant,
+ onContentChange,
+ blockId,
+ time,
+}) => {
+ const [isExpanded, setIsExpanded] = React.useState(false);
+ const isMobile = useUIStore((state) => state.isMobile);
+ const showActivityHeaderTimestamps = useUIStore((state) => state.showActivityHeaderTimestamps);
+
+ const summary = React.useMemo(() => getReasoningSummary(text), [text]);
+ const { label, Icon } = variantConfig[variant];
+ const timeStart = typeof time?.start === 'number' && Number.isFinite(time.start) ? time.start : undefined;
+ const timeEnd = typeof time?.end === 'number' && Number.isFinite(time.end) ? time.end : undefined;
+ const endedTimestampText = React.useMemo(() => {
+ if (typeof timeEnd !== 'number') {
+ return null;
+ }
+
+ const formatted = formatTimestampForDisplay(timeEnd);
+ return formatted.length > 0 ? formatted : null;
+ }, [timeEnd]);
+
+ React.useEffect(() => {
+ if (text.trim().length === 0) {
+ return;
+ }
+ onContentChange?.('structural');
+ }, [onContentChange, isExpanded, text]);
+
+ if (!text || text.trim().length === 0) {
+ return null;
+ }
+
+ return (
+
+
setIsExpanded((prev) => !prev)}
+ >
+
+
+
+
+
+
+ {isExpanded ? : }
+
+
+
{label}
+
+
+ {(summary || typeof timeStart === 'number' || endedTimestampText) ? (
+
+ {summary ? {summary} : null}
+ {typeof timeStart === 'number' ? (
+
+
+
+
+ {!isMobile && endedTimestampText && showActivityHeaderTimestamps ? (
+
+ {endedTimestampText}
+
+ ) : null}
+
+ ) : null}
+ {typeof timeStart !== 'number' && !isMobile && endedTimestampText && showActivityHeaderTimestamps ? (
+
+ {endedTimestampText}
+
+ ) : null}
+
+ ) : null}
+
+
+ {isExpanded && (
+
+
+ {text}
+
+
+ )}
+
+ );
+};
+
+type ReasoningPartProps = {
+ part: Part;
+ onContentChange?: (reason?: ContentChangeReason) => void;
+ messageId: string;
+};
+
+const ReasoningPart: React.FC = ({
+ part,
+ onContentChange,
+ messageId,
+}) => {
+ const partWithText = part as PartWithText;
+ const rawText = partWithText.text || partWithText.content || '';
+ const textContent = React.useMemo(() => cleanReasoningText(rawText), [rawText]);
+ const time = partWithText.time;
+
+ // Show reasoning even if time.end isn't set yet (during streaming)
+ // Only hide if there's no text content
+ if (!textContent || textContent.trim().length === 0) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const formatReasoningText = (text: string): string => cleanReasoningText(text);
+
+export default ReasoningPart;
diff --git a/ui/src/components/chat/message/parts/ToolPart.tsx b/ui/src/components/chat/message/parts/ToolPart.tsx
new file mode 100644
index 0000000..8d49c78
--- /dev/null
+++ b/ui/src/components/chat/message/parts/ToolPart.tsx
@@ -0,0 +1,1938 @@
+
+import React from 'react';
+import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
+import { RiAiAgentLine, RiArrowDownSLine, RiArrowRightSLine, RiBookLine, RiExternalLinkLine, RiFileEditLine, RiFileList2Line, RiFileSearchLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGlobalLine, RiListCheck2, RiListCheck3, RiMenuSearchLine, RiPencilLine, RiSurveyLine, RiTaskLine, RiTerminalBoxLine, RiToolsLine } from '@remixicon/react';
+import { File as PierreFile, PatchDiff } from '@pierre/diffs/react';
+import { cn } from '@/lib/utils';
+import { formatTimestampForDisplay } from '../timeFormat';
+import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
+import { getToolMetadata, getLanguageFromExtension, isImageFile, getImageMimeType } from '@/lib/toolHelpers';
+import type { ToolPart as ToolPartType, ToolState as ToolStateUnion } from '@opencode-ai/sdk/v2';
+import { toolDisplayStyles } from '@/lib/typography';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
+import { useDirectoryStore } from '@/stores/useDirectoryStore';
+import { useSessionStore } from '@/stores/useSessionStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { opencodeClient } from '@/lib/opencode/client';
+import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
+import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
+import type { ToolPopupContent } from '../types';
+import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
+import { getDefaultTheme } from '@/lib/theme/themes';
+
+import {
+ renderListOutput,
+ renderGrepOutput,
+ renderGlobOutput,
+ renderTodoOutput,
+ renderWebSearchOutput,
+ formatEditOutput,
+ detectLanguageFromOutput,
+ formatInputForDisplay,
+ parseReadToolOutput,
+} from '../toolRenderers';
+import { DiffViewToggle, type DiffViewMode } from '../DiffViewToggle';
+import { VirtualizedCodeBlock, type CodeLine } from './VirtualizedCodeBlock';
+
+type ToolStateWithMetadata = ToolStateUnion & { metadata?: Record; input?: Record; output?: string; error?: string; time?: { start: number; end?: number } };
+
+interface ToolPartProps {
+ part: ToolPartType;
+ isExpanded: boolean;
+ onToggle: (toolId: string) => void;
+ syntaxTheme: { [key: string]: React.CSSProperties };
+ isMobile: boolean;
+ onContentChange?: (reason?: ContentChangeReason) => void;
+ onShowPopup?: (content: ToolPopupContent) => void;
+ hasPrevTool?: boolean;
+ hasNextTool?: boolean;
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const getToolIcon = (toolName: string) => {
+ const iconClass = 'h-3.5 w-3.5 flex-shrink-0';
+ const tool = toolName.toLowerCase();
+
+ if (tool === 'edit' || tool === 'multiedit' || tool === 'apply_patch' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
+ return ;
+ }
+ if (tool === 'write' || tool === 'create' || tool === 'file_write') {
+ return ;
+ }
+ if (tool === 'read' || tool === 'view' || tool === 'file_read' || tool === 'cat') {
+ return ;
+ }
+ if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal') {
+ return ;
+ }
+ if (tool === 'list' || tool === 'ls' || tool === 'dir' || tool === 'list_files') {
+ return ;
+ }
+ if (tool === 'search' || tool === 'grep' || tool === 'find' || tool === 'ripgrep') {
+ return ;
+ }
+ if (tool === 'glob') {
+ return ;
+ }
+ if (tool === 'fetch' || tool === 'curl' || tool === 'wget' || tool === 'webfetch') {
+ return ;
+ }
+ if (
+ tool === 'web-search' ||
+ tool === 'websearch' ||
+ tool === 'search_web' ||
+ tool === 'codesearch' ||
+ tool === 'google' ||
+ tool === 'bing' ||
+ tool === 'duckduckgo' ||
+ tool === 'perplexity'
+ ) {
+ return ;
+ }
+ if (tool === 'todowrite' || tool === 'todoread') {
+ return ;
+ }
+ if (tool === 'structuredoutput' || tool === 'structured_output') {
+ return ;
+ }
+ if (tool === 'skill') {
+ return ;
+ }
+ if (tool === 'task') {
+ return ;
+ }
+ if (tool === 'question') {
+ return ;
+ }
+ if (tool === 'plan_enter') {
+ return ;
+ }
+ if (tool === 'plan_exit') {
+ return ;
+ }
+ if (tool.startsWith('git')) {
+ return ;
+ }
+ return ;
+};
+
+const formatDuration = (start: number, end?: number, now: number = Date.now()) => {
+ const duration = end ? end - start : now - start;
+ const seconds = duration / 1000;
+
+ const displaySeconds = seconds < 0.05 && end !== undefined ? 0.1 : seconds;
+ return `${displaySeconds.toFixed(1)}s`;
+};
+
+const LiveDuration: React.FC<{ start: number; end?: number; active: boolean }> = ({ start, end, active }) => {
+ const [now, setNow] = React.useState(() => Date.now());
+
+ React.useEffect(() => {
+ if (!active) {
+ return;
+ }
+ const timer = window.setInterval(() => {
+ setNow(Date.now());
+ }, 100);
+ return () => window.clearInterval(timer);
+ }, [active]);
+
+ return <>{formatDuration(start, end, now)}>;
+};
+
+const parseDiffStats = (metadata?: Record): { added: number; removed: number } | null => {
+ if (!metadata?.diff || typeof metadata.diff !== 'string') return null;
+
+ const lines = metadata.diff.split('\n');
+ let added = 0;
+ let removed = 0;
+
+ for (const line of lines) {
+ if (line.startsWith('+') && !line.startsWith('+++')) added++;
+ if (line.startsWith('-') && !line.startsWith('---')) removed++;
+ }
+
+ if (added === 0 && removed === 0) return null;
+ return { added, removed };
+};
+
+const extractFirstChangedLineFromDiff = (diffText: string): number | undefined => {
+ if (!diffText || typeof diffText !== 'string') {
+ return undefined;
+ }
+
+ const lines = diffText.split('\n');
+ let currentNewLine: number | undefined;
+ let firstHunkStart: number | undefined;
+
+ for (const rawLine of lines) {
+ const line = rawLine.replace(/\r$/, '');
+ const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
+ if (hunkMatch) {
+ const parsed = Number.parseInt(hunkMatch[1] ?? '', 10);
+ if (Number.isFinite(parsed)) {
+ currentNewLine = Math.max(1, parsed);
+ if (!Number.isFinite(firstHunkStart)) {
+ firstHunkStart = currentNewLine;
+ }
+ }
+ continue;
+ }
+
+ if (currentNewLine === undefined || !Number.isFinite(currentNewLine)) {
+ continue;
+ }
+
+ if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('diff ')) {
+ continue;
+ }
+
+ if (line.startsWith('+')) {
+ return currentNewLine;
+ }
+
+ if (line.startsWith(' ')) {
+ currentNewLine += 1;
+ continue;
+ }
+
+ if (line.startsWith('-') || line.startsWith('\\')) {
+ continue;
+ }
+ }
+
+ return firstHunkStart;
+};
+
+const getFirstChangedLineFromMetadata = (tool: string, metadata?: Record): number | undefined => {
+ if (!metadata || (tool !== 'edit' && tool !== 'multiedit' && tool !== 'apply_patch')) {
+ return undefined;
+ }
+
+ if (typeof metadata.diff === 'string') {
+ const line = extractFirstChangedLineFromDiff(metadata.diff);
+ if (Number.isFinite(line)) {
+ return line;
+ }
+ }
+
+ const files = Array.isArray(metadata.files) ? metadata.files : [];
+ const firstFile = files[0] as { diff?: unknown } | undefined;
+ if (typeof firstFile?.diff === 'string') {
+ const line = extractFirstChangedLineFromDiff(firstFile.diff);
+ if (Number.isFinite(line)) {
+ return line;
+ }
+ }
+
+ return undefined;
+};
+
+const getPrimaryDiffFromMetadata = (
+ tool: string,
+ metadata?: Record,
+ preferredPath?: string,
+): string | undefined => {
+ if (!metadata || (tool !== 'edit' && tool !== 'multiedit' && tool !== 'apply_patch')) {
+ return undefined;
+ }
+
+ const files = Array.isArray(metadata.files) ? metadata.files : [];
+ if (files.length > 0) {
+ const preferred = typeof preferredPath === 'string' && preferredPath.length > 0
+ ? preferredPath
+ : undefined;
+ const matched = preferred
+ ? files.find((file) => {
+ if (!file || typeof file !== 'object') {
+ return false;
+ }
+ const candidate = file as { relativePath?: unknown; filePath?: unknown };
+ return candidate.relativePath === preferred || candidate.filePath === preferred;
+ })
+ : files[0];
+
+ if (matched && typeof matched === 'object') {
+ const patch = (matched as { diff?: unknown }).diff;
+ if (typeof patch === 'string' && patch.trim().length > 0) {
+ return patch;
+ }
+ }
+ }
+
+ if (typeof metadata.diff === 'string' && metadata.diff.trim().length > 0) {
+ return metadata.diff;
+ }
+
+ return undefined;
+};
+
+const getRelativePath = (absolutePath: string, currentDirectory: string): string => {
+ if (absolutePath.startsWith(currentDirectory)) {
+ const relativePath = absolutePath.substring(currentDirectory.length);
+
+ return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
+ }
+
+ return absolutePath;
+};
+
+const usePierreThemeConfig = () => {
+ const themeSystem = useOptionalThemeSystem();
+ const fallbackLightTheme = React.useMemo(() => getDefaultTheme(false), []);
+ const fallbackDarkTheme = React.useMemo(() => getDefaultTheme(true), []);
+
+ const availableThemes = React.useMemo(
+ () => themeSystem?.availableThemes ?? [fallbackLightTheme, fallbackDarkTheme],
+ [fallbackDarkTheme, fallbackLightTheme, themeSystem?.availableThemes],
+ );
+ const lightThemeId = themeSystem?.lightThemeId ?? fallbackLightTheme.metadata.id;
+ const darkThemeId = themeSystem?.darkThemeId ?? fallbackDarkTheme.metadata.id;
+
+ const lightTheme = React.useMemo(
+ () => availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? fallbackLightTheme,
+ [availableThemes, fallbackLightTheme, lightThemeId],
+ );
+ const darkTheme = React.useMemo(
+ () => availableThemes.find((theme) => theme.metadata.id === darkThemeId) ?? fallbackDarkTheme,
+ [availableThemes, darkThemeId, fallbackDarkTheme],
+ );
+
+ React.useEffect(() => {
+ ensurePierreThemeRegistered(lightTheme);
+ ensurePierreThemeRegistered(darkTheme);
+ }, [darkTheme, lightTheme]);
+
+ const currentVariant = themeSystem?.currentTheme.metadata.variant ?? 'light';
+
+ return {
+ pierreTheme: { light: lightTheme.metadata.id, dark: darkTheme.metadata.id },
+ pierreThemeType: currentVariant === 'dark' ? ('dark' as const) : ('light' as const),
+ };
+};
+
+// Parse question tool output: "User has answered your questions: "Q1"="A1", "Q2"="A2". You can now..."
+const parseQuestionOutput = (output: string): Array<{ question: string; answer: string }> | null => {
+ const match = output.match(/^User has answered your questions:\s*(.+?)\.\s*You can now/s);
+ if (!match) return null;
+
+ const pairs: Array<{ question: string; answer: string }> = [];
+ const content = match[1];
+
+ // Match "question"="answer" pairs, handling multiline answers
+ const pairRegex = /"([^"]+)"="([^"]*(?:[^"\\]|\\.)*)"/g;
+ let pairMatch;
+ while ((pairMatch = pairRegex.exec(content)) !== null) {
+ pairs.push({
+ question: pairMatch[1],
+ answer: pairMatch[2],
+ });
+ }
+
+ return pairs.length > 0 ? pairs : null;
+};
+
+const formatStructuredOutputDescription = (input: Record | undefined, output: unknown): string => {
+ if (typeof output === 'string' && output.trim().length > 0) {
+ const maxLength = 100;
+ const text = output.trim();
+ return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
+ }
+
+ if (!input || typeof input !== 'object') {
+ return 'Result';
+ }
+
+ const rawValue = Object.prototype.hasOwnProperty.call(input, 'result') ? input.result : input;
+
+ const toPreview = (value: unknown): string => {
+ if (typeof value === 'string') {
+ return value;
+ }
+ if (typeof value === 'number' || typeof value === 'boolean') {
+ return String(value);
+ }
+ if (Array.isArray(value)) {
+ const joined = value
+ .map((item) => (typeof item === 'string' ? item : JSON.stringify(item)))
+ .join(', ');
+ return joined;
+ }
+ if (value && typeof value === 'object') {
+ const record = value as Record;
+ if (typeof record.subject === 'string' && record.subject.trim().length > 0) {
+ return record.subject;
+ }
+ if (typeof record.title === 'string' && record.title.trim().length > 0) {
+ return record.title;
+ }
+ return JSON.stringify(value);
+ }
+ return '';
+ };
+
+ const preview = toPreview(rawValue).trim();
+ if (!preview) {
+ return 'Result';
+ }
+
+ const maxLength = 100;
+ const truncated = preview.length > maxLength ? `${preview.substring(0, maxLength)}...` : preview;
+ return truncated;
+};
+
+const getToolDescriptionPath = (part: ToolPartType, state: ToolStateUnion, currentDirectory: string): string | null => {
+ const stateWithData = state as ToolStateWithMetadata;
+ const metadata = stateWithData.metadata;
+ const input = stateWithData.input;
+
+ if (part.tool === 'apply_patch') {
+ const files = Array.isArray(metadata?.files) ? metadata?.files : [];
+ const firstFile = files[0] as { relativePath?: string; filePath?: string } | undefined;
+ const filePath = firstFile?.relativePath || firstFile?.filePath;
+ if (files.length > 1) return null;
+ if (typeof filePath === 'string') {
+ return getRelativePath(filePath, currentDirectory);
+ }
+ return null;
+ }
+
+ if ((part.tool === 'edit' || part.tool === 'multiedit') && input) {
+ const filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
+ if (typeof filePath === 'string') {
+ return getRelativePath(filePath, currentDirectory);
+ }
+ }
+
+ if (['write', 'create', 'file_write', 'read', 'view', 'file_read', 'cat'].includes(part.tool) && input) {
+ const filePath = input?.filePath || input?.file_path || input?.path;
+ if (typeof filePath === 'string') {
+ return getRelativePath(filePath, currentDirectory);
+ }
+ }
+
+ return null;
+};
+
+const getToolDescription = (part: ToolPartType, state: ToolStateUnion, currentDirectory: string): string => {
+ const stateWithData = state as ToolStateWithMetadata;
+ const metadata = stateWithData.metadata;
+ const input = stateWithData.input;
+ const tool = part.tool.toLowerCase();
+
+ if (tool === 'structuredoutput' || tool === 'structured_output') {
+ return formatStructuredOutputDescription(input, stateWithData.output);
+ }
+
+ const filePathLabel = getToolDescriptionPath(part, state, currentDirectory);
+ if (filePathLabel) {
+ return filePathLabel;
+ }
+
+ if (part.tool === 'apply_patch') {
+ const files = Array.isArray(metadata?.files) ? metadata?.files : [];
+ if (files.length > 1) {
+ return `${files.length} files`;
+ }
+ return 'Patch';
+ }
+
+ // Question tool: show "Asked N question(s)"
+ if (part.tool === 'question' && input?.questions && Array.isArray(input.questions)) {
+ const count = input.questions.length;
+ return `Asked ${count} question${count !== 1 ? 's' : ''}`;
+ }
+
+ if (part.tool === 'bash' && input?.command && typeof input.command === 'string') {
+ const firstLine = input.command.split('\n')[0];
+ return firstLine.substring(0, 100);
+ }
+
+ if (part.tool === 'task' && input?.description && typeof input.description === 'string') {
+ return input.description.substring(0, 80);
+ }
+
+ if (part.tool === 'skill' && input?.name && typeof input.name === 'string') {
+ return input.name;
+ }
+
+ if (part.tool === 'plan_enter') {
+ return 'Switching to planning';
+ }
+
+ if (part.tool === 'plan_exit') {
+ return 'Switching to building';
+ }
+
+ const desc = input?.description || metadata?.description || ('title' in state && state.title) || '';
+ return typeof desc === 'string' ? desc : '';
+};
+
+interface ToolScrollableSectionProps {
+ children: React.ReactNode;
+ maxHeightClass?: string;
+ className?: string;
+ outerClassName?: string;
+ disableHorizontal?: boolean;
+}
+
+const ToolScrollableSection: React.FC = ({
+ children,
+ maxHeightClass = 'max-h-[60vh]',
+ className,
+ outerClassName,
+ disableHorizontal = false,
+}) => (
+
+
+ {children}
+
+
+);
+
+type TaskToolSummaryEntry = {
+ id?: string;
+ tool?: string;
+ state?: {
+ status?: string;
+ title?: string;
+ };
+};
+
+type SessionMessageWithParts = {
+ info?: {
+ role?: string;
+ };
+ parts?: Array<{
+ id?: string;
+ type?: string;
+ tool?: string;
+ state?: {
+ status?: string;
+ title?: string;
+ };
+ }>;
+};
+
+const EMPTY_SESSION_MESSAGES: SessionMessageWithParts[] = [];
+
+const readTaskSessionIdFromOutput = (output: string | undefined): string | undefined => {
+ if (typeof output !== 'string' || output.trim().length === 0) {
+ return undefined;
+ }
+ const parsedMetadata = parseTaskMetadataBlock(output);
+ if (parsedMetadata.sessionId) {
+ return parsedMetadata.sessionId;
+ }
+ const match = output.match(/task_id:\s*([a-zA-Z0-9_]+)/);
+ const candidate = match?.[1];
+ return typeof candidate === 'string' && candidate.trim().length > 0 ? candidate : undefined;
+};
+
+const buildTaskSummaryEntriesFromSession = (messages: SessionMessageWithParts[]): TaskToolSummaryEntry[] => {
+ const entries: TaskToolSummaryEntry[] = [];
+
+ for (const message of messages) {
+ if (message?.info?.role !== 'assistant') {
+ continue;
+ }
+ const parts = Array.isArray(message.parts) ? message.parts : [];
+ for (const part of parts) {
+ if (part?.type !== 'tool') {
+ continue;
+ }
+ const toolName = typeof part.tool === 'string' ? part.tool.toLowerCase() : '';
+ if (!toolName || toolName === 'task' || toolName === 'todowrite' || toolName === 'todoread') {
+ continue;
+ }
+ entries.push({
+ id: part.id,
+ tool: part.tool,
+ state: {
+ status: part.state?.status,
+ title: part.state?.title,
+ },
+ });
+ }
+ }
+
+ return entries;
+};
+
+const getTaskSummaryLabel = (entry: TaskToolSummaryEntry): string => {
+ const title = entry.state?.title;
+ if (typeof title === 'string' && title.trim().length > 0) {
+ return title;
+ }
+ if (typeof entry.tool === 'string' && entry.tool.trim().length > 0) {
+ return entry.tool;
+ }
+ return 'tool';
+};
+
+const FILE_PATH_LABEL_TOOLS = new Set([
+ 'read',
+ 'view',
+ 'file_read',
+ 'cat',
+ 'write',
+ 'create',
+ 'file_write',
+ 'edit',
+ 'multiedit',
+ 'apply_patch',
+]);
+
+const shouldRenderGitPathLabel = (toolName: string, label: string): boolean => {
+ if (!FILE_PATH_LABEL_TOOLS.has(toolName.toLowerCase())) {
+ return false;
+ }
+
+ const trimmed = label.trim();
+ if (!trimmed || trimmed === 'Patch' || /^\d+\s+files$/.test(trimmed)) {
+ return false;
+ }
+
+ return trimmed.includes('/') || trimmed.includes('\\');
+};
+
+const stripTaskMetadataFromOutput = (output: string): string => {
+ // Strip only a trailing ... block.
+ return output.replace(/\n*[\s\S]*?<\/task_metadata>\s*$/i, '').trimEnd();
+};
+
+const normalizeTaskSummaryEntries = (value: unknown): TaskToolSummaryEntry[] => {
+ if (!Array.isArray(value)) {
+ return [];
+ }
+
+ const normalized: TaskToolSummaryEntry[] = [];
+ for (const entry of value) {
+ if (typeof entry === 'string') {
+ normalized.push({
+ tool: 'tool',
+ state: { status: 'completed', title: entry },
+ });
+ continue;
+ }
+
+ if (!entry || typeof entry !== 'object') {
+ continue;
+ }
+
+ const record = entry as {
+ id?: unknown;
+ tool?: unknown;
+ title?: unknown;
+ status?: unknown;
+ state?: { status?: unknown; title?: unknown };
+ };
+
+ const stateStatus = typeof record.state?.status === 'string' ? record.state.status : undefined;
+ const stateTitle = typeof record.state?.title === 'string' ? record.state.title : undefined;
+ const status = stateStatus ?? (typeof record.status === 'string' ? record.status : undefined);
+ const title = stateTitle ?? (typeof record.title === 'string' ? record.title : undefined);
+
+ normalized.push({
+ id: typeof record.id === 'string' ? record.id : undefined,
+ tool: typeof record.tool === 'string' ? record.tool : 'tool',
+ state: {
+ status,
+ title,
+ },
+ });
+ }
+
+ return normalized;
+};
+
+const parseTaskMetadataBlock = (output: string | undefined): {
+ sessionId?: string;
+ summaryEntries: TaskToolSummaryEntry[];
+} => {
+ if (typeof output !== 'string' || output.trim().length === 0) {
+ return { summaryEntries: [] };
+ }
+
+ const blockMatch = output.match(/\s*([\s\S]*?)\s*<\/task_metadata>/i);
+ if (!blockMatch?.[1]) {
+ return { summaryEntries: [] };
+ }
+
+ const raw = blockMatch[1].trim();
+ if (!raw) {
+ return { summaryEntries: [] };
+ }
+
+ try {
+ const parsed = JSON.parse(raw) as {
+ sessionId?: unknown;
+ sessionID?: unknown;
+ summary?: unknown;
+ entries?: unknown;
+ tools?: unknown;
+ calls?: unknown;
+ };
+
+ const summaryEntries = normalizeTaskSummaryEntries(
+ parsed.summary ?? parsed.entries ?? parsed.tools ?? parsed.calls
+ );
+
+ const sessionId =
+ (typeof parsed.sessionId === 'string' && parsed.sessionId.trim().length > 0
+ ? parsed.sessionId.trim()
+ : undefined) ??
+ (typeof parsed.sessionID === 'string' && parsed.sessionID.trim().length > 0
+ ? parsed.sessionID.trim()
+ : undefined);
+
+ return { sessionId, summaryEntries };
+ } catch {
+ return { summaryEntries: [] };
+ }
+};
+
+const TaskToolSummary: React.FC<{
+ entries: TaskToolSummaryEntry[];
+ isExpanded: boolean;
+ isMobile: boolean;
+ hasPrevTool: boolean;
+ hasNextTool: boolean;
+ output?: string;
+ sessionId?: string;
+ onShowPopup?: (content: ToolPopupContent) => void;
+ input?: Record;
+}> = ({ entries, isExpanded, isMobile, hasPrevTool, hasNextTool, output, sessionId, onShowPopup, input }) => {
+ const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
+ const displayEntries = React.useMemo(() => {
+ const nonPending = entries.filter((entry) => entry.state?.status !== 'pending');
+ return nonPending.length > 0 ? nonPending : entries;
+ }, [entries]);
+
+ const trimmedOutput = typeof output === 'string'
+ ? stripTaskMetadataFromOutput(output)
+ : '';
+ const hasOutput = trimmedOutput.length > 0;
+ const [isOutputExpanded, setIsOutputExpanded] = React.useState(false);
+
+ const handleOpenSession = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ if (sessionId) {
+ setCurrentSession(sessionId);
+ }
+ };
+
+ const agentType = typeof input?.subagent_type === 'string'
+ ? input.subagent_type
+ : 'subagent';
+
+ if (displayEntries.length === 0 && !hasOutput && !sessionId) {
+ return null;
+ }
+
+ const visibleEntries = isExpanded ? displayEntries : displayEntries.slice(-6);
+ const hiddenCount = Math.max(0, displayEntries.length - visibleEntries.length);
+
+ return (
+
+ {displayEntries.length > 0 ? (
+
+
+ {hiddenCount > 0 ? (
+
+{hiddenCount} more…
+ ) : null}
+
+ {visibleEntries.map((entry, idx) => {
+ const toolName = typeof entry.tool === 'string' && entry.tool.trim().length > 0 ? entry.tool : 'tool';
+ const label = getTaskSummaryLabel(entry);
+ const status = entry.state?.status;
+
+ const displayName = getToolMetadata(toolName).displayName;
+
+ return (
+
+ {getToolIcon(toolName)}
+ {displayName}
+ {status !== 'error' && shouldRenderGitPathLabel(toolName, label) ? (
+ renderPathLikeGitChanges(label)
+ ) : (
+ {label}
+ )}
+
+ );
+ })}
+
+
+ ) : null}
+
+ {sessionId && (
+
event.stopPropagation()}
+ onClick={handleOpenSession}
+ >
+
+ Open {agentType.charAt(0).toUpperCase() + agentType.slice(1)} subtask
+
+ )}
+
+ {hasOutput ? (
+
0 || sessionId) && 'pt-1')}
+ >
+
event.stopPropagation()}
+ onClick={(event) => {
+ event.stopPropagation();
+ setIsOutputExpanded((prev) => !prev);
+ }}
+ >
+ {isOutputExpanded ? (
+
+ ) : (
+
+ )}
+ Output
+
+ {isOutputExpanded ? (
+
+
+
+
+
+ ) : null}
+
+ ) : null}
+
+ );
+};
+
+interface DiffPreviewProps {
+ diff: string;
+ pierreTheme: { light: string; dark: string };
+ pierreThemeType: 'light' | 'dark';
+ diffViewMode: DiffViewMode;
+}
+
+const TOOL_DIFF_UNSAFE_CSS = `
+ [data-diff-header],
+ [data-diff] {
+ [data-separator] {
+ height: 24px !important;
+ }
+ }
+`;
+
+const TOOL_DIFF_METRICS = {
+ hunkLineCount: 50,
+ lineHeight: 24,
+ diffHeaderHeight: 44,
+ hunkSeparatorHeight: 24,
+ fileGap: 0,
+};
+
+type DiffPatchEntry = {
+ id: string;
+ title: string;
+ patch: string;
+};
+
+const renderPathLikeGitChanges = (path: string, grow = true) => {
+ const lastSlash = path.lastIndexOf('/');
+ if (lastSlash === -1) {
+ return (
+
+ {path}
+
+ );
+ }
+
+ const dir = path.slice(0, lastSlash);
+ const name = path.slice(lastSlash + 1);
+
+ return (
+
+
+ {dir}
+
+
+ /
+ {name}
+
+
+ );
+};
+
+const getDiffPatchEntries = (
+ metadata: Record | undefined,
+ fallbackDiff: string,
+ currentDirectory: string,
+): DiffPatchEntry[] => {
+ const files = Array.isArray(metadata?.files) ? metadata.files : [];
+
+ const entries = files
+ .map((file, index) => {
+ if (!file || typeof file !== 'object') {
+ return null;
+ }
+
+ const record = file as { relativePath?: unknown; filePath?: unknown; diff?: unknown };
+ const patch = typeof record.diff === 'string' ? record.diff.trim() : '';
+ if (!patch) {
+ return null;
+ }
+
+ const rawPath = typeof record.relativePath === 'string'
+ ? record.relativePath
+ : typeof record.filePath === 'string'
+ ? record.filePath
+ : `File ${index + 1}`;
+
+ const title = typeof rawPath === 'string'
+ ? getRelativePath(rawPath, currentDirectory)
+ : `File ${index + 1}`;
+
+ return {
+ id: `${title}-${index}`,
+ title,
+ patch,
+ } satisfies DiffPatchEntry;
+ })
+ .filter((entry): entry is DiffPatchEntry => entry !== null);
+
+ if (entries.length > 0) {
+ return entries;
+ }
+
+ return [
+ {
+ id: 'diff-0',
+ title: 'Diff',
+ patch: fallbackDiff,
+ },
+ ];
+};
+
+const DiffPreview: React.FC = React.memo(({ diff, pierreTheme, pierreThemeType, diffViewMode }) => {
+ return (
+
+ );
+});
+
+DiffPreview.displayName = 'DiffPreview';
+
+interface WriteInputPreviewProps {
+ content: string;
+ filePath?: string;
+ displayPath: string;
+ pierreTheme: { light: string; dark: string };
+ pierreThemeType: 'light' | 'dark';
+}
+
+const WriteInputPreview: React.FC = React.memo(({
+ content,
+ filePath,
+ displayPath,
+ pierreTheme,
+ pierreThemeType,
+}) => {
+ const language = React.useMemo(
+ () => getLanguageFromExtension(filePath ?? '') || detectLanguageFromOutput(content, 'write', filePath ? { filePath } : undefined),
+ [content, filePath]
+ );
+
+ const lineCount = Math.max(content.split('\n').length, 1);
+ const headerLineLabel = lineCount === 1 ? 'line 1' : `lines 1-${lineCount}`;
+
+ return (
+
+
+ {renderPathLikeGitChanges(displayPath)}
+ ({headerLineLabel})
+
+
+
+ );
+});
+
+WriteInputPreview.displayName = 'WriteInputPreview';
+
+// ── PERF-007: Read tool output with virtualised highlighting ─────────
+interface ReadToolVirtualizedProps {
+ outputString: string;
+ input?: Record;
+ syntaxTheme: { [key: string]: React.CSSProperties };
+ toolName: string;
+ currentDirectory: string;
+ pierreTheme: { light: string; dark: string };
+ pierreThemeType: 'light' | 'dark';
+ renderScrollableBlock: (
+ content: React.ReactNode,
+ options?: { maxHeightClass?: string; className?: string; disableHorizontal?: boolean; outerClassName?: string }
+ ) => React.ReactNode;
+}
+
+const ReadToolVirtualized: React.FC = React.memo(({
+ outputString,
+ input,
+ syntaxTheme,
+ toolName,
+ currentDirectory,
+ pierreTheme,
+ pierreThemeType,
+ renderScrollableBlock,
+}) => {
+ const parsedReadOutput = React.useMemo(() => parseReadToolOutput(outputString), [outputString]);
+
+ const language = React.useMemo(() => {
+ const contentForLanguage = parsedReadOutput.lines.map((l) => l.text).join('\n');
+ return detectLanguageFromOutput(contentForLanguage, toolName, input as Record);
+ }, [parsedReadOutput, toolName, input]);
+
+ const rawFilePath =
+ typeof input?.filePath === 'string'
+ ? input.filePath
+ : typeof input?.file_path === 'string'
+ ? input.file_path
+ : typeof input?.path === 'string'
+ ? input.path
+ : 'read-output';
+ const displayPath = getRelativePath(rawFilePath, currentDirectory);
+
+ const codeLines: CodeLine[] = React.useMemo(() => parsedReadOutput.lines.map((line) => ({
+ text: line.text,
+ lineNumber: line.lineNumber,
+ isInfo: line.isInfo,
+ })), [parsedReadOutput]);
+
+ if (parsedReadOutput.type === 'file') {
+ const fileContent = parsedReadOutput.lines.map((line) => line.text).join('\n');
+ const lineCount = Math.max(parsedReadOutput.lines.length, 1);
+ const headerLineLabel = lineCount === 1 ? 'line 1' : `lines 1-${lineCount}`;
+ return renderScrollableBlock(
+
+
+ {renderPathLikeGitChanges(displayPath)}
+ ({headerLineLabel})
+
+
+
,
+ { className: 'p-1' }
+ ) as React.ReactElement;
+ }
+
+ return renderScrollableBlock(
+ ,
+ { className: 'p-1' }
+ ) as React.ReactElement;
+});
+
+ReadToolVirtualized.displayName = 'ReadToolVirtualized';
+
+interface ImagePreviewProps {
+ content: string;
+ filePath: string;
+ displayPath: string;
+}
+
+const ImagePreview: React.FC = React.memo(({ content, filePath, displayPath }) => {
+ const mimeType = getImageMimeType(filePath);
+ const isSvg = filePath.toLowerCase().endsWith('.svg');
+
+ // For SVG, content might be raw XML, otherwise assume base64
+ const imageSrc = React.useMemo(() => {
+ if (isSvg && !content.startsWith('data:')) {
+ // Raw SVG content
+ return `data:image/svg+xml;base64,${btoa(content)}`;
+ }
+ if (content.startsWith('data:')) {
+ return content;
+ }
+ // Assume base64 encoded
+ return `data:${mimeType};base64,${content}`;
+ }, [content, mimeType, isSvg]);
+
+ return (
+
+
+ {renderPathLikeGitChanges(displayPath)}
+
+
+
+
+
+ );
+});
+
+ImagePreview.displayName = 'ImagePreview';
+
+interface ToolExpandedContentProps {
+ part: ToolPartType;
+ state: ToolStateUnion;
+ syntaxTheme: { [key: string]: React.CSSProperties };
+ isMobile: boolean;
+ currentDirectory: string;
+ onShowPopup?: (content: ToolPopupContent) => void;
+ hasPrevTool: boolean;
+ hasNextTool: boolean;
+}
+
+const ToolExpandedContent: React.FC = React.memo(({
+ part,
+ state,
+ syntaxTheme,
+ isMobile,
+ currentDirectory,
+ onShowPopup,
+ hasPrevTool,
+ hasNextTool,
+}) => {
+ const { pierreTheme, pierreThemeType } = usePierreThemeConfig();
+ const [diffViewMode, setDiffViewMode] = React.useState('unified');
+ const stateWithData = state as ToolStateWithMetadata;
+ const metadata = stateWithData.metadata;
+ const input = stateWithData.input;
+ const rawOutput = stateWithData.output;
+ const hasStringOutput = typeof rawOutput === 'string' && rawOutput.length > 0;
+ const outputString = typeof rawOutput === 'string' ? rawOutput : '';
+
+ const diffContent = typeof metadata?.diff === 'string' ? (metadata.diff as string) : null;
+ const diffEntries = React.useMemo(
+ () => (diffContent ? getDiffPatchEntries(metadata, diffContent, currentDirectory) : []),
+ [currentDirectory, diffContent, metadata]
+ );
+ const writeFilePath = part.tool === 'write'
+ ? typeof input?.filePath === 'string'
+ ? input.filePath
+ : typeof input?.file_path === 'string'
+ ? input.file_path
+ : typeof input?.path === 'string'
+ ? input.path
+ : undefined
+ : undefined;
+ const writeInputContent = part.tool === 'write'
+ ? typeof (input as { content?: unknown })?.content === 'string'
+ ? (input as { content?: string }).content
+ : typeof (input as { text?: unknown })?.text === 'string'
+ ? (input as { text?: string }).text
+ : null
+ : null;
+ const shouldShowWriteInputPreview = part.tool === 'write' && !!writeInputContent;
+ const isWriteImageFile = writeFilePath ? isImageFile(writeFilePath) : false;
+ const writeDisplayPath = shouldShowWriteInputPreview
+ ? (writeFilePath ? getRelativePath(writeFilePath, currentDirectory) : 'New file')
+ : null;
+
+ const inputTextContent = React.useMemo(() => {
+ if (!input || typeof input !== 'object' || Object.keys(input).length === 0) {
+ return '';
+ }
+
+ if ('command' in input && typeof input.command === 'string' && part.tool === 'bash') {
+ return formatInputForDisplay(input, part.tool);
+ }
+
+ if (typeof (input as { content?: unknown }).content === 'string') {
+ return (input as { content?: string }).content ?? '';
+ }
+
+ return formatInputForDisplay(input, part.tool);
+ }, [input, part.tool]);
+ const hasInputText = part.tool !== 'apply_patch' && inputTextContent.trim().length > 0;
+
+ React.useEffect(() => {
+ setDiffViewMode('unified');
+ }, [part.id]);
+
+ const renderScrollableBlock = (
+ content: React.ReactNode,
+ options?: { maxHeightClass?: string; className?: string; disableHorizontal?: boolean; outerClassName?: string }
+ ) => (
+
+ {content}
+
+ );
+
+ const renderResultContent = () => {
+ // Question tool: show parsed Q&A summary
+ if (part.tool === 'question') {
+ if (state.status === 'completed' && hasStringOutput) {
+ const parsedQA = parseQuestionOutput(outputString);
+ if (parsedQA && parsedQA.length > 0) {
+ return renderScrollableBlock(
+
+ {parsedQA.map((qa, index) => (
+
+
{qa.question}
+
{qa.answer}
+
+ ))}
+
,
+ { maxHeightClass: 'max-h-[40vh]' }
+ );
+ }
+ }
+
+ if (state.status === 'error' && 'error' in state) {
+ return (
+
+
Error:
+
+ {state.error}
+
+
+ );
+ }
+
+ return Awaiting response...
;
+ }
+
+ if (part.tool === 'todowrite' || part.tool === 'todoread') {
+ if (state.status === 'completed' && hasStringOutput) {
+ const todoContent = renderTodoOutput(outputString, { unstyled: true });
+ return renderScrollableBlock(
+ todoContent ?? (
+ Unable to parse todo list
+ )
+ );
+ }
+
+ if (state.status === 'error' && 'error' in state) {
+ return (
+
+
Error:
+
+ {state.error}
+
+
+ );
+ }
+
+ return Processing todo list...
;
+ }
+
+ if (part.tool === 'list' && hasStringOutput) {
+ const listOutput = renderListOutput(outputString, { unstyled: true });
+ return renderScrollableBlock(
+ listOutput ?? (
+
+ {outputString}
+
+ )
+ );
+ }
+
+ if (part.tool === 'grep' && hasStringOutput) {
+ const grepOutput = renderGrepOutput(outputString, isMobile, { unstyled: true });
+ return renderScrollableBlock(
+ grepOutput ?? (
+
+ {outputString}
+
+ )
+ );
+ }
+
+ if (part.tool === 'glob' && hasStringOutput) {
+ const globOutput = renderGlobOutput(outputString, isMobile, { unstyled: true });
+ return renderScrollableBlock(
+ globOutput ?? (
+
+ {outputString}
+
+ )
+ );
+ }
+
+ if (part.tool === 'task' && hasStringOutput) {
+ return renderScrollableBlock(
+
+
+
+ );
+ }
+
+ if ((part.tool === 'web-search' || part.tool === 'websearch' || part.tool === 'search_web') && hasStringOutput) {
+ const webSearchContent = renderWebSearchOutput(outputString, syntaxTheme, { unstyled: true });
+ return renderScrollableBlock(
+ webSearchContent ?? (
+
+ {outputString}
+
+ )
+ );
+ }
+
+ if (part.tool === 'codesearch' && hasStringOutput) {
+ return renderScrollableBlock(
+
+
+
+ );
+ }
+
+ if (part.tool === 'skill' && hasStringOutput) {
+ return renderScrollableBlock(
+
+
+
+ );
+ }
+
+ if ((part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') && diffEntries.length > 0) {
+ return renderScrollableBlock(
+
+ {diffEntries.map((entry) => (
+
+ {diffEntries.length > 1 ? (
+
+ {renderPathLikeGitChanges(entry.title)}
+
+ ) : null}
+
+
+ ))}
+
,
+ { className: 'p-1' }
+ );
+ }
+
+ if (hasStringOutput && outputString.trim()) {
+ if (part.tool === 'read') {
+ return ;
+ }
+
+ return renderScrollableBlock(
+
+ {formatEditOutput(outputString, part.tool, metadata)}
+ ,
+ { className: 'p-1' }
+ );
+ }
+
+ return renderScrollableBlock(
+ No output produced
,
+ { maxHeightClass: 'max-h-60' }
+ );
+ };
+
+ return (
+
+
+ {(part.tool === 'todowrite' || part.tool === 'todoread' || part.tool === 'question') ? (
+ renderResultContent()
+ ) : (
+ <>
+ {shouldShowWriteInputPreview && isWriteImageFile ? (
+
+ {renderScrollableBlock(
+
+ )}
+
+ ) : shouldShowWriteInputPreview ? (
+
+ {renderScrollableBlock(
+
+ )}
+
+ ) : hasInputText ? (
+
+ {renderScrollableBlock(
+
+ {inputTextContent}
+ ,
+ { maxHeightClass: 'max-h-60', className: 'tool-input-surface' }
+ )}
+
+ ) : null}
+
+ {part.tool !== 'write' && state.status === 'completed' && 'output' in state && (
+
+
+
+ Result:
+
+ {(part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') && diffContent ? (
+
+ ) : null}
+
+ {renderResultContent()}
+
+ )}
+
+ {state.status === 'error' && 'error' in state && (
+
+
Error:
+
+ {state.error}
+
+
+ )}
+ >
+ )}
+
+ );
+});
+
+ToolExpandedContent.displayName = 'ToolExpandedContent';
+
+const ToolPart: React.FC = ({
+ part,
+ isExpanded,
+ onToggle,
+ syntaxTheme,
+ isMobile,
+ onContentChange,
+ onShowPopup,
+ hasPrevTool = false,
+ hasNextTool = false,
+}) => {
+ const state = part.state;
+ const currentDirectory = useDirectoryStore((s) => s.currentDirectory);
+ const showActivityHeaderTimestamps = useUIStore((store) => store.showActivityHeaderTimestamps);
+
+ const isTaskTool = part.tool.toLowerCase() === 'task';
+
+ const status = state.status as string | undefined;
+ const isFinalized = status === 'completed' || status === 'error';
+ const isActive = status === 'running' || status === 'pending' || status === 'started';
+ const isError = state.status === 'error';
+
+
+
+ const shouldNotifyStructuralChange = isFinalized || isTaskTool;
+
+ React.useEffect(() => {
+ if (!shouldNotifyStructuralChange) {
+ return;
+ }
+ if (typeof isExpanded === 'boolean') {
+ onContentChange?.('structural');
+ }
+ }, [isExpanded, onContentChange, shouldNotifyStructuralChange]);
+
+ const stateWithData = state as ToolStateWithMetadata;
+ const metadata = stateWithData.metadata;
+ const input = stateWithData.input;
+ const time = stateWithData.time;
+
+ const [pinnedTime, setPinnedTime] = React.useState<{ start?: number; end?: number }>({});
+
+ React.useEffect(() => {
+ setPinnedTime({});
+ }, [part.id]);
+
+ React.useEffect(() => {
+ setPinnedTime((prev) => {
+ const next = { ...prev };
+ let changed = false;
+
+ if (typeof time?.start === 'number' && (typeof prev.start !== 'number' || time.start < prev.start)) {
+ next.start = time.start;
+ changed = true;
+ }
+
+ if (typeof time?.end === 'number' && prev.end !== time.end) {
+ next.end = time.end;
+ changed = true;
+ }
+
+ return changed ? next : prev;
+ });
+ }, [time?.end, time?.start]);
+
+ const effectiveTimeStart = pinnedTime.start ?? time?.start;
+ const effectiveTimeEnd = pinnedTime.end ?? time?.end;
+
+ const endedTimestampText = React.useMemo(() => {
+ if (typeof effectiveTimeEnd !== 'number' || !Number.isFinite(effectiveTimeEnd)) {
+ return null;
+ }
+
+ const formatted = formatTimestampForDisplay(effectiveTimeEnd);
+ return formatted.length > 0 ? formatted : null;
+ }, [effectiveTimeEnd]);
+
+ const taskOutputString = React.useMemo(() => {
+ return typeof stateWithData.output === 'string' ? stateWithData.output : undefined;
+ }, [stateWithData.output]);
+
+ const parsedTaskMetadata = React.useMemo(() => {
+ return parseTaskMetadataBlock(taskOutputString);
+ }, [taskOutputString]);
+
+ const taskSessionId = React.useMemo(() => {
+ if (!isTaskTool) {
+ return undefined;
+ }
+ const candidate = metadata as { sessionId?: string } | undefined;
+ if (typeof candidate?.sessionId === 'string' && candidate.sessionId.trim().length > 0) {
+ return candidate.sessionId;
+ }
+ if (parsedTaskMetadata.sessionId) {
+ return parsedTaskMetadata.sessionId;
+ }
+ return readTaskSessionIdFromOutput(taskOutputString);
+ }, [isTaskTool, metadata, parsedTaskMetadata.sessionId, taskOutputString]);
+
+ const childSessionMessages = useSessionStore(
+ React.useCallback((store) => {
+ if (!taskSessionId) {
+ return EMPTY_SESSION_MESSAGES;
+ }
+ return (store.messages.get(taskSessionId) as SessionMessageWithParts[] | undefined) ?? EMPTY_SESSION_MESSAGES;
+ }, [taskSessionId])
+ );
+
+ const metadataTaskSummaryEntries = React.useMemo(() => {
+ if (!isTaskTool) {
+ return [];
+ }
+ const candidateSummary = (metadata as { summary?: unknown; entries?: unknown; tools?: unknown; calls?: unknown } | undefined);
+ const normalized = normalizeTaskSummaryEntries(
+ candidateSummary?.summary ?? candidateSummary?.entries ?? candidateSummary?.tools ?? candidateSummary?.calls
+ );
+
+ if (normalized.length > 0) {
+ return normalized;
+ }
+
+ return parsedTaskMetadata.summaryEntries;
+ }, [isTaskTool, metadata, parsedTaskMetadata.summaryEntries]);
+
+ const childSessionTaskSummaryEntries = React.useMemo(() => {
+ if (!isTaskTool || !taskSessionId) {
+ return [];
+ }
+ if (!Array.isArray(childSessionMessages) || childSessionMessages.length === 0) {
+ return [];
+ }
+ return buildTaskSummaryEntriesFromSession(childSessionMessages);
+ }, [childSessionMessages, isTaskTool, taskSessionId]);
+
+ const taskSummaryEntries = React.useMemo(() => {
+ if (childSessionTaskSummaryEntries.length > 0) {
+ return childSessionTaskSummaryEntries;
+ }
+ return metadataTaskSummaryEntries;
+ }, [childSessionTaskSummaryEntries, metadataTaskSummaryEntries]);
+
+ const fetchedTaskSessionsRef = React.useRef>(new Set());
+ React.useEffect(() => {
+ if (!isTaskTool || !taskSessionId) {
+ return;
+ }
+ if (childSessionTaskSummaryEntries.length > 0) {
+ return;
+ }
+ if (fetchedTaskSessionsRef.current.has(taskSessionId)) {
+ return;
+ }
+
+ fetchedTaskSessionsRef.current.add(taskSessionId);
+ let cancelled = false;
+
+ void opencodeClient
+ .getSessionMessages(taskSessionId, 500)
+ .then((messages) => {
+ if (cancelled || !Array.isArray(messages)) {
+ return;
+ }
+ if (messages.length === 0) {
+ fetchedTaskSessionsRef.current.delete(taskSessionId);
+ return;
+ }
+ useSessionStore.getState().syncMessages(taskSessionId, messages);
+ })
+ .catch(() => {
+ fetchedTaskSessionsRef.current.delete(taskSessionId);
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [childSessionTaskSummaryEntries.length, isTaskTool, taskSessionId]);
+
+
+ const taskSummaryLenRef = React.useRef(taskSummaryEntries.length);
+ React.useEffect(() => {
+ if (!isTaskTool) {
+ return;
+ }
+ if (taskSummaryLenRef.current === taskSummaryEntries.length) {
+ return;
+ }
+ taskSummaryLenRef.current = taskSummaryEntries.length;
+ onContentChange?.('structural');
+ }, [isTaskTool, onContentChange, taskSummaryEntries.length]);
+
+ const diffStats = (part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') ? parseDiffStats(metadata) : null;
+ const descriptionPath = getToolDescriptionPath(part, state, currentDirectory);
+ const description = getToolDescription(part, state, currentDirectory);
+ const displayName = getToolMetadata(part.tool).displayName;
+
+ // Get justification text (tool title/description) when setting is enabled
+ const showTextJustificationActivity = useUIStore((state) => state.showTextJustificationActivity);
+ const justificationText = React.useMemo(() => {
+ if (!showTextJustificationActivity) return null;
+ if (part.tool === 'apply_patch') return null;
+ if (part.tool.toLowerCase() === 'structuredoutput' || part.tool.toLowerCase() === 'structured_output') return null;
+ // Get title or description from state - this is the "yapping" text like "Shows system information"
+ const title = (stateWithData as { title?: string }).title;
+ if (typeof title === 'string' && title.trim().length > 0) {
+ return title;
+ }
+ const inputDesc = input?.description;
+ if (typeof inputDesc === 'string' && inputDesc.trim().length > 0) {
+ return inputDesc;
+ }
+ return null;
+ }, [showTextJustificationActivity, part.tool, stateWithData, input]);
+
+ const runtime = React.useContext(RuntimeAPIContext);
+
+ const handleMainClick = (e: { stopPropagation: () => void }) => {
+ if (isTaskTool || !runtime?.editor) {
+ onToggle(part.id);
+ return;
+ }
+
+ let filePath: unknown;
+ let targetLine: number | undefined;
+ let toolDiff: string | undefined;
+ if (part.tool === 'edit' || part.tool === 'multiedit') {
+ filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
+ targetLine = getFirstChangedLineFromMetadata(part.tool, metadata);
+ if (typeof filePath === 'string') {
+ toolDiff = getPrimaryDiffFromMetadata(part.tool, metadata, filePath);
+ }
+ } else if (part.tool === 'apply_patch') {
+ const files = Array.isArray(metadata?.files) ? metadata?.files : [];
+ const firstFile = files[0] as { relativePath?: string; filePath?: string } | undefined;
+ filePath = firstFile?.relativePath || firstFile?.filePath;
+ targetLine = getFirstChangedLineFromMetadata(part.tool, metadata);
+ if (typeof filePath === 'string') {
+ toolDiff = getPrimaryDiffFromMetadata(part.tool, metadata, filePath);
+ }
+ } else if (['write', 'create', 'file_write', 'read', 'view', 'file_read', 'cat'].includes(part.tool)) {
+ filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
+ }
+
+ if (typeof filePath === 'string') {
+ e.stopPropagation();
+ let absolutePath = filePath;
+ if (!filePath.startsWith('/')) {
+ absolutePath = currentDirectory.endsWith('/') ? currentDirectory + filePath : currentDirectory + '/' + filePath;
+ }
+ if (runtime.runtime.isVSCode && toolDiff && (part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch')) {
+ const label = `${getRelativePath(absolutePath, currentDirectory)} (changes)`;
+ void runtime.editor.openDiff('', absolutePath, label, { line: targetLine, patch: toolDiff });
+ return;
+ }
+ runtime.editor.openFile(absolutePath, targetLine);
+ } else {
+ onToggle(part.id);
+ }
+ };
+
+ const handleMainKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key !== 'Enter' && event.key !== ' ') {
+ return;
+ }
+ event.preventDefault();
+ handleMainClick(event);
+ };
+
+ if (!isFinalized && !isActive && !isTaskTool) {
+ return null;
+ }
+
+ return (
+
+ {}
+
+
+ {}
+
{ event.stopPropagation(); onToggle(part.id); }}
+ >
+ {}
+
+ {getToolIcon(part.tool)}
+
+ {}
+
+ {isExpanded ? : }
+
+
+
+ {displayName}
+
+
+
+
+
+ {justificationText && (
+
+ {justificationText}
+
+ )}
+ {!justificationText && description && (
+ descriptionPath && description === descriptionPath ? (
+ renderPathLikeGitChanges(descriptionPath, false)
+ ) : (
+
+ {description}
+
+ )
+ )}
+ {diffStats && (
+
+ +{diffStats.added}
+ {' '}
+ -{diffStats.removed}
+
+ )}
+
+ {typeof effectiveTimeStart === 'number' ? (
+
+
+
+
+ {!isMobile && endedTimestampText && showActivityHeaderTimestamps ? (
+
+ {endedTimestampText}
+
+ ) : null}
+
+ ) : null}
+ {typeof effectiveTimeStart !== 'number' && !isMobile && endedTimestampText && showActivityHeaderTimestamps ? (
+
+ {endedTimestampText}
+
+ ) : null}
+
+
+
+ {}
+ {isTaskTool && (taskSummaryEntries.length > 0 || isActive || isFinalized || taskSessionId) ? (
+
+ ) : null}
+
+ {!isTaskTool && isExpanded ? (
+
+ ) : null}
+
+ );
+};
+
+export default ToolPart;
diff --git a/ui/src/components/chat/message/parts/UserTextPart.tsx b/ui/src/components/chat/message/parts/UserTextPart.tsx
new file mode 100644
index 0000000..19623ec
--- /dev/null
+++ b/ui/src/components/chat/message/parts/UserTextPart.tsx
@@ -0,0 +1,164 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+import type { Part } from '@opencode-ai/sdk/v2';
+import type { AgentMentionInfo } from '../types';
+import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
+import { useUIStore } from '@/stores/useUIStore';
+
+type PartWithText = Part & { text?: string; content?: string; value?: string };
+
+type UserTextPartProps = {
+ part: Part;
+ messageId: string;
+ isMobile: boolean;
+ agentMention?: AgentMentionInfo;
+};
+
+const buildMentionUrl = (name: string): string => {
+ const encoded = encodeURIComponent(name);
+ return `https://opencode.ai/docs/agents/#${encoded}`;
+};
+
+const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' => {
+ return mode === 'markdown' ? 'markdown' : 'plain';
+};
+
+const UserTextPart: React.FC = ({ part, messageId, agentMention }) => {
+ const CLAMP_LINES = 2;
+ const partWithText = part as PartWithText;
+ const rawText = partWithText.text;
+ const textContent = typeof rawText === 'string' ? rawText : partWithText.content || partWithText.value || '';
+
+ const [isExpanded, setIsExpanded] = React.useState(false);
+ const [isTruncated, setIsTruncated] = React.useState(false);
+ const [collapseZoneHeight, setCollapseZoneHeight] = React.useState(0);
+ const userMessageRenderingMode = useUIStore((state) => state.userMessageRenderingMode);
+ const normalizedRenderingMode = normalizeUserMessageRenderingMode(userMessageRenderingMode);
+ const textRef = React.useRef(null);
+
+ const hasActiveSelectionInElement = React.useCallback((element: HTMLElement): boolean => {
+ if (typeof window === 'undefined') {
+ return false;
+ }
+
+ const selection = window.getSelection();
+ if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
+ return false;
+ }
+
+ const range = selection.getRangeAt(0);
+ return element.contains(range.startContainer) || element.contains(range.endContainer);
+ }, []);
+
+ React.useEffect(() => {
+ const el = textRef.current;
+ if (!el) return;
+
+ const checkTruncation = () => {
+ if (!isExpanded) {
+ setIsTruncated(el.scrollHeight > el.clientHeight);
+ }
+
+ const styles = window.getComputedStyle(el);
+ const lineHeight = parseFloat(styles.lineHeight);
+ const fontSize = parseFloat(styles.fontSize);
+ const fallbackLineHeight = isFinite(fontSize) ? fontSize * 1.4 : 20;
+ const resolvedLineHeight = isFinite(lineHeight) ? lineHeight : fallbackLineHeight;
+ setCollapseZoneHeight(Math.max(1, Math.round(resolvedLineHeight * CLAMP_LINES)));
+ };
+
+ checkTruncation();
+
+ const resizeObserver = new ResizeObserver(checkTruncation);
+ resizeObserver.observe(el);
+
+ return () => resizeObserver.disconnect();
+ }, [textContent, isExpanded]);
+
+ const handleClick = React.useCallback((event: React.MouseEvent) => {
+ const element = textRef.current;
+ if (!element) {
+ return;
+ }
+
+ if (hasActiveSelectionInElement(element)) {
+ return;
+ }
+
+ if (!isExpanded) {
+ if (isTruncated) {
+ setIsExpanded(true);
+ }
+ return;
+ }
+
+ const clickY = event.clientY - element.getBoundingClientRect().top;
+ if (clickY <= collapseZoneHeight) {
+ setIsExpanded(false);
+ }
+ }, [collapseZoneHeight, hasActiveSelectionInElement, isExpanded, isTruncated]);
+
+ const processedMarkdownContent = React.useMemo(() => {
+ if (!agentMention?.token || !textContent.includes(agentMention.token)) {
+ return textContent;
+ }
+
+ const mentionHtml = `${agentMention.token} `;
+ return textContent.replace(agentMention.token, mentionHtml);
+ }, [agentMention, textContent]);
+
+ const plainTextContent = React.useMemo(() => {
+ if (!agentMention?.token || !textContent.includes(agentMention.token)) {
+ return textContent;
+ }
+
+ const idx = textContent.indexOf(agentMention.token);
+ const before = textContent.slice(0, idx);
+ const after = textContent.slice(idx + agentMention.token.length);
+ return (
+ <>
+ {before}
+ event.stopPropagation()}
+ >
+ {agentMention.token}
+
+ {after}
+ >
+ );
+ }, [agentMention, textContent]);
+
+ if (!textContent || textContent.trim().length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {normalizedRenderingMode === 'markdown' ? (
+
+ ) : (
+ plainTextContent
+ )}
+
+
+ );
+};
+
+export default React.memo(UserTextPart);
diff --git a/ui/src/components/chat/message/parts/VirtualizedCodeBlock.tsx b/ui/src/components/chat/message/parts/VirtualizedCodeBlock.tsx
new file mode 100644
index 0000000..179c366
--- /dev/null
+++ b/ui/src/components/chat/message/parts/VirtualizedCodeBlock.tsx
@@ -0,0 +1,339 @@
+/**
+ * VirtualizedCodeBlock — PERF-007
+ *
+ * Replaces per-line with:
+ * 1. ONE Prism.highlight() call to tokenize all code at once
+ * 2. @tanstack/react-virtual to only render visible rows
+ *
+ * This drops mount cost from O(N * Prism) to O(1 * Prism) + O(visible_rows).
+ * For a 2000-line file, ~2000 SyntaxHighlighter instances → ~30 plain s.
+ */
+
+import React from 'react';
+import { useVirtualizer } from '@tanstack/react-virtual';
+import Prism from 'prismjs';
+
+// Ensure common languages are loaded (react-syntax-highlighter lazy-loads them,
+// but we call Prism directly so we need them registered).
+import 'prismjs/components/prism-markup';
+import 'prismjs/components/prism-markup-templating';
+import 'prismjs/components/prism-typescript';
+import 'prismjs/components/prism-javascript';
+import 'prismjs/components/prism-jsx';
+import 'prismjs/components/prism-tsx';
+import 'prismjs/components/prism-css';
+import 'prismjs/components/prism-json';
+import 'prismjs/components/prism-bash';
+import 'prismjs/components/prism-python';
+import 'prismjs/components/prism-rust';
+import 'prismjs/components/prism-go';
+import 'prismjs/components/prism-java';
+import 'prismjs/components/prism-c';
+import 'prismjs/components/prism-cpp';
+import 'prismjs/components/prism-csharp';
+import 'prismjs/components/prism-ruby';
+import 'prismjs/components/prism-yaml';
+import 'prismjs/components/prism-toml';
+import 'prismjs/components/prism-markdown';
+import 'prismjs/components/prism-sql';
+import 'prismjs/components/prism-diff';
+import 'prismjs/components/prism-docker';
+import 'prismjs/components/prism-swift';
+import 'prismjs/components/prism-kotlin';
+import 'prismjs/components/prism-lua';
+import 'prismjs/components/prism-php';
+import 'prismjs/components/prism-scss';
+
+// ── Threshold: files smaller than this render without virtualization ──
+const VIRTUALIZE_THRESHOLD = 80;
+const ROW_HEIGHT = 20; // px — matches typography-code line-height
+
+// ── Types ────────────────────────────────────────────────────────────
+export interface CodeLine {
+ text: string;
+ lineNumber?: number | null;
+ isInfo?: boolean;
+ /** For diff lines */
+ type?: 'context' | 'added' | 'removed';
+}
+
+interface VirtualizedCodeBlockProps {
+ lines: CodeLine[];
+ language: string;
+ syntaxTheme: Record
;
+ /** Max visible height in CSS (default: 60vh) */
+ maxHeight?: string;
+ /** Show line numbers (default: true) */
+ showLineNumbers?: boolean;
+ /** Styles per line type (for diffs) */
+ lineStyles?: (line: CodeLine) => React.CSSProperties | undefined;
+}
+
+const toKebabCase = (value: string): string => value.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
+
+const styleObjectToCss = (style: React.CSSProperties): string => {
+ return Object.entries(style)
+ .filter(([, v]) => v !== undefined && v !== null)
+ .map(([k, v]) => `${toKebabCase(k)}:${String(v)};`)
+ .join('');
+};
+
+const buildSelectorList = (rawKey: string): string[] => {
+ return rawKey
+ .split(',')
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .flatMap((selector) => {
+ if (selector.startsWith('.token')) {
+ return [`.oc-virtualized-prism ${selector}`];
+ }
+ if (selector.startsWith('token.')) {
+ return [`.oc-virtualized-prism .${selector}`];
+ }
+ if (/^[a-z0-9_-]+$/i.test(selector)) {
+ return [`.oc-virtualized-prism .token.${selector}`];
+ }
+ if (selector.includes('token')) {
+ return [`.oc-virtualized-prism ${selector}`];
+ }
+ return [];
+ });
+};
+
+const buildPrismThemeCss = (theme: Record): string => {
+ const rules: string[] = [];
+ Object.entries(theme).forEach(([rawKey, style]) => {
+ const selectors = buildSelectorList(rawKey);
+ if (selectors.length === 0) {
+ return;
+ }
+ const css = styleObjectToCss(style);
+ if (!css) {
+ return;
+ }
+ rules.push(`${selectors.join(',')}{${css}}`);
+ });
+ return rules.join('\n');
+};
+
+const LANGUAGE_ALIASES: Record = {
+ text: 'plain',
+ plaintext: 'plain',
+ shell: 'bash',
+ sh: 'bash',
+ zsh: 'bash',
+ patch: 'diff',
+ dockerfile: 'docker',
+ js: 'javascript',
+ ts: 'typescript',
+};
+
+const normalizeLanguage = (language: string): string => {
+ const lower = language.toLowerCase();
+ return LANGUAGE_ALIASES[lower] ?? lower;
+};
+
+const HIGHLIGHT_CACHE_MAX = 5000;
+const highlightCache = new Map();
+
+const highlightLine = (text: string, language: string): string => {
+ const normalizedLanguage = normalizeLanguage(language);
+ const cacheKey = `${normalizedLanguage}\n${text}`;
+ const cached = highlightCache.get(cacheKey);
+ if (cached !== undefined) {
+ return cached;
+ }
+
+ const grammar = Prism.languages[normalizedLanguage] ?? Prism.languages.text;
+ if (!grammar) {
+ const escaped = escapeHtml(text);
+ highlightCache.set(cacheKey, escaped);
+ return escaped;
+ }
+
+ try {
+ const highlighted = Prism.highlight(text, grammar, normalizedLanguage);
+ if (highlightCache.size >= HIGHLIGHT_CACHE_MAX) {
+ const oldestKey = highlightCache.keys().next().value;
+ if (typeof oldestKey === 'string') {
+ highlightCache.delete(oldestKey);
+ }
+ }
+ highlightCache.set(cacheKey, highlighted);
+ return highlighted;
+ } catch {
+ const escaped = escapeHtml(text);
+ highlightCache.set(cacheKey, escaped);
+ return escaped;
+ }
+};
+
+function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+}
+
+// ── Component ────────────────────────────────────────────────────────
+export const VirtualizedCodeBlock: React.FC = React.memo((props) => {
+ const {
+ lines,
+ language,
+ syntaxTheme,
+ maxHeight = '60vh',
+ showLineNumbers = true,
+ lineStyles,
+ } = props;
+ const prismThemeCss = React.useMemo(() => buildPrismThemeCss(syntaxTheme), [syntaxTheme]);
+
+ const shouldVirtualize = lines.length > VIRTUALIZE_THRESHOLD;
+
+ // ── Small file: render directly (no virtualizer overhead) ──
+ if (!shouldVirtualize) {
+ return (
+
+ {prismThemeCss ? : null}
+ {lines.map((line, idx) => (
+
+ ))}
+
+ );
+ }
+
+ // ── Large file: virtualise ──
+ return (
+
+ );
+});
+
+VirtualizedCodeBlock.displayName = 'VirtualizedCodeBlock';
+
+// ── Virtualised container (extracted so the hook is top-level) ────────
+interface VirtualizedRowsProps {
+ lines: CodeLine[];
+ language: string;
+ prismThemeCss: string;
+ maxHeight: string;
+ showLineNumbers: boolean;
+ lineStyles?: (line: CodeLine) => React.CSSProperties | undefined;
+}
+
+const VirtualizedRows: React.FC = React.memo(({
+ lines,
+ language,
+ prismThemeCss,
+ maxHeight,
+ showLineNumbers,
+ lineStyles,
+}) => {
+ const parentRef = React.useRef(null);
+
+ const virtualizer = useVirtualizer({
+ count: lines.length,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => ROW_HEIGHT,
+ overscan: 20, // render 20 extra rows above/below viewport
+ });
+
+ return (
+
+ {prismThemeCss ? : null}
+
+ {virtualizer.getVirtualItems().map((vItem) => {
+ const line = lines[vItem.index];
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+});
+
+VirtualizedRows.displayName = 'VirtualizedRows';
+
+// ── Single row ───────────────────────────────────────────────────────
+interface RowProps {
+ line: CodeLine;
+ language: string;
+ showLineNumbers: boolean;
+ style?: React.CSSProperties;
+}
+
+const Row: React.FC = React.memo(({ line, language, showLineNumbers, style }) => {
+ const html = React.useMemo(() => highlightLine(line.text, language), [line.text, language]);
+
+ return (
+
+ {showLineNumbers && (
+
+ {!line.isInfo && line.lineNumber != null ? line.lineNumber : ''}
+
+ )}
+
+ {line.isInfo ? (
+
+ {line.text}
+
+ ) : (
+
+ )}
+
+
+ );
+});
+
+Row.displayName = 'VirtualizedCodeBlock.Row';
diff --git a/ui/src/components/chat/message/parts/WorkingPlaceholder.tsx b/ui/src/components/chat/message/parts/WorkingPlaceholder.tsx
new file mode 100644
index 0000000..5b603ce
--- /dev/null
+++ b/ui/src/components/chat/message/parts/WorkingPlaceholder.tsx
@@ -0,0 +1,232 @@
+import React from 'react';
+import { Text } from '@/components/ui/text';
+
+interface WorkingPlaceholderProps {
+ isWorking: boolean;
+ statusText: string | null;
+ isGenericStatus?: boolean;
+ isWaitingForPermission?: boolean;
+ retryInfo?: { attempt?: number; next?: number } | null;
+}
+
+const STATUS_DISPLAY_TIME_MS = 1200;
+
+const EPOCH_SECONDS_THRESHOLD = 1_000_000_000;
+const EPOCH_MILLISECONDS_THRESHOLD = 1_000_000_000_000;
+
+const toRetryTargetTimestamp = (next: number): number => {
+ if (next >= EPOCH_MILLISECONDS_THRESHOLD) {
+ return next;
+ }
+ if (next >= EPOCH_SECONDS_THRESHOLD) {
+ return next * 1000;
+ }
+ return Date.now() + next;
+};
+
+const formatRetryCountdown = (seconds: number): string => {
+ if (seconds < 60) {
+ return `${seconds}s`;
+ }
+
+ if (seconds < 3600) {
+ const minutes = Math.floor(seconds / 60);
+ const remainderSeconds = seconds % 60;
+ return remainderSeconds > 0 ? `${minutes}m ${remainderSeconds}s` : `${minutes}m`;
+ }
+
+ if (seconds < 86400) {
+ const hours = Math.floor(seconds / 3600);
+ const remainderMinutes = Math.floor((seconds % 3600) / 60);
+ return remainderMinutes > 0 ? `${hours}h ${remainderMinutes}m` : `${hours}h`;
+ }
+
+ const days = Math.floor(seconds / 86400);
+ const remainderHours = Math.floor((seconds % 86400) / 3600);
+ if (remainderHours > 0) {
+ return `${days}d ${remainderHours}h`;
+ }
+
+ return `${days}d`;
+
+};
+
+export function WorkingPlaceholder({
+ isWorking,
+ statusText,
+ isGenericStatus,
+ isWaitingForPermission,
+ retryInfo,
+}: WorkingPlaceholderProps) {
+ const [displayedText, setDisplayedText] = React.useState(null);
+ const [displayedPermission, setDisplayedPermission] = React.useState(false);
+
+ const statusShownAtRef = React.useRef(0);
+ const queuedStatusRef = React.useRef<{ text: string; permission: boolean } | null>(null);
+ const processQueueTimerRef = React.useRef | null>(null);
+
+ // Countdown state for retry mode
+ const [retryCountdown, setRetryCountdown] = React.useState(null);
+
+ React.useEffect(() => {
+ const rawNext = retryInfo?.next;
+ if (!rawNext || rawNext <= 0) {
+ setRetryCountdown(null);
+ return;
+ }
+
+ const retryTargetAt = toRetryTargetTimestamp(rawNext);
+
+ const update = () => {
+ const remaining = Math.max(0, retryTargetAt - Date.now());
+ setRetryCountdown(Math.ceil(remaining / 1000));
+ };
+
+ update();
+ const id = setInterval(update, 500);
+ return () => clearInterval(id);
+ }, [retryInfo?.next, retryInfo?.attempt]);
+
+ const clearTimers = React.useCallback(() => {
+ if (processQueueTimerRef.current) {
+ clearTimeout(processQueueTimerRef.current);
+ processQueueTimerRef.current = null;
+ }
+ }, []);
+
+ const showStatus = React.useCallback((text: string, permission: boolean) => {
+ clearTimers();
+ queuedStatusRef.current = null;
+ setDisplayedText(text);
+ setDisplayedPermission(permission);
+ statusShownAtRef.current = Date.now();
+ }, [clearTimers]);
+
+ const scheduleQueueProcess = React.useCallback(() => {
+ if (processQueueTimerRef.current) return;
+ const elapsed = Date.now() - statusShownAtRef.current;
+ const remaining = Math.max(0, STATUS_DISPLAY_TIME_MS - elapsed);
+ processQueueTimerRef.current = setTimeout(() => {
+ processQueueTimerRef.current = null;
+
+ const queued = queuedStatusRef.current;
+ if (queued) {
+ showStatus(queued.text, queued.permission);
+ }
+ }, remaining);
+ }, [showStatus]);
+
+ React.useEffect(() => {
+ if (!isWorking) {
+ clearTimers();
+ queuedStatusRef.current = null;
+ setDisplayedText(null);
+ setDisplayedPermission(false);
+ return;
+ }
+
+ // Retry state has its own display — skip the normal queue
+ if (retryInfo) {
+ clearTimers();
+ queuedStatusRef.current = null;
+ return;
+ }
+
+ const incomingText = isWaitingForPermission ? 'waiting for permission' : statusText;
+ const incomingPermission = Boolean(isWaitingForPermission);
+ const incomingGeneric = Boolean(isGenericStatus) && !incomingPermission;
+
+ if (!incomingText) {
+ return;
+ }
+
+ if (!displayedText) {
+ showStatus(incomingText, incomingPermission);
+ return;
+ }
+
+ if (incomingText === displayedText && incomingPermission === displayedPermission) {
+ return;
+ }
+
+ // Ignore generic churn.
+ if (incomingGeneric) {
+ return;
+ }
+
+ const elapsed = Date.now() - statusShownAtRef.current;
+ if (elapsed >= STATUS_DISPLAY_TIME_MS) {
+ showStatus(incomingText, incomingPermission);
+ return;
+ }
+
+ queuedStatusRef.current = { text: incomingText, permission: incomingPermission };
+ scheduleQueueProcess();
+ }, [
+ isWorking,
+ statusText,
+ isGenericStatus,
+ isWaitingForPermission,
+ retryInfo,
+ displayedText,
+ displayedPermission,
+ clearTimers,
+ showStatus,
+ scheduleQueueProcess,
+ ]);
+
+ React.useEffect(() => () => clearTimers(), [clearTimers]);
+
+ if (!isWorking) {
+ return null;
+ }
+
+ // Retry state: show countdown and attempt info
+ if (retryInfo) {
+ const attemptLabel = retryInfo.attempt && retryInfo.attempt > 1 ? ` (attempt ${retryInfo.attempt})` : '';
+ const countdownLabel = retryCountdown !== null && retryCountdown > 0
+ ? ` in ${formatRetryCountdown(retryCountdown)}`
+ : '';
+ const retryText = `Retrying${countdownLabel}${attemptLabel}...`;
+
+ return (
+
+
+
+ {retryText}
+
+
+
+ );
+ }
+
+ if (!displayedText) {
+ return null;
+ }
+
+ const label = displayedText.charAt(0).toUpperCase() + displayedText.slice(1);
+ const displayText = `${label}...`;
+
+ return (
+
+
+
+ {displayText}
+
+
+
+ );
+}
diff --git a/ui/src/components/chat/message/timeFormat.ts b/ui/src/components/chat/message/timeFormat.ts
new file mode 100644
index 0000000..bd92f08
--- /dev/null
+++ b/ui/src/components/chat/message/timeFormat.ts
@@ -0,0 +1,48 @@
+const pad2 = (value: number): string => String(value).padStart(2, '0');
+
+const isSameDay = (left: Date, right: Date): boolean => {
+ return (
+ left.getFullYear() === right.getFullYear() &&
+ left.getMonth() === right.getMonth() &&
+ left.getDate() === right.getDate()
+ );
+};
+
+const isYesterday = (date: Date, now: Date): boolean => {
+ const yesterday = new Date(now);
+ yesterday.setDate(now.getDate() - 1);
+ return isSameDay(date, yesterday);
+};
+
+const isValidTimestamp = (timestamp: number): boolean => {
+ return Number.isFinite(timestamp) && !Number.isNaN(new Date(timestamp).getTime());
+};
+
+export const formatTimestampForDisplay = (timestamp: number): string => {
+ if (!isValidTimestamp(timestamp)) {
+ return '';
+ }
+
+ const date = new Date(timestamp);
+ const now = new Date();
+
+ const timePart = `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
+
+ if (isSameDay(date, now)) {
+ return timePart;
+ }
+
+ if (isYesterday(date, now)) {
+ return `Yesterday ${timePart}`;
+ }
+
+ const monthPart = date.toLocaleString(undefined, { month: 'short' });
+ const dayPart = date.getDate();
+ const datePart = `${monthPart} ${dayPart}`;
+
+ if (date.getFullYear() === now.getFullYear()) {
+ return `${datePart}, ${timePart}`;
+ }
+
+ return `${datePart}, ${date.getFullYear()}, ${timePart}`;
+};
diff --git a/ui/src/components/chat/message/toolRenderers.tsx b/ui/src/components/chat/message/toolRenderers.tsx
new file mode 100644
index 0000000..ed6fc99
--- /dev/null
+++ b/ui/src/components/chat/message/toolRenderers.tsx
@@ -0,0 +1,722 @@
+import { RiCheckLine } from '@remixicon/react';
+
+import { cn } from '@/lib/utils';
+import { typography } from '@/lib/typography';
+import { formatToolInput, detectToolOutputLanguage } from '@/lib/toolHelpers';
+import { SimpleMarkdownRenderer } from '../MarkdownRenderer';
+
+const cleanOutput = (output: string) => {
+ let cleaned = output.replace(/^\s*\n?/, '').replace(/\n?<\/file>\s*$/, '');
+ cleaned = cleaned.replace(/^\s*\d{5}\|\s?/gm, '');
+ return cleaned.trim();
+};
+
+export const hasLspDiagnostics = (output: string): boolean => {
+ if (!output) return false;
+ return output.includes('') || output.includes('This file has errors') || output.includes('please fix');
+};
+
+const stripLspDiagnostics = (output: string): string => {
+ if (!output) return '';
+ return output.replace(/This file has errors.*?<\/file_diagnostics>/s, '').trim();
+};
+
+const formatInputForDisplay = (input: Record, toolName?: string) => {
+ if (!input || typeof input !== 'object') {
+ return String(input);
+ }
+ return formatToolInput(input, toolName || '');
+};
+
+export const formatEditOutput = (output: string, toolName: string, metadata?: Record): string => {
+ let cleaned = cleanOutput(output);
+
+ if ((toolName === 'edit' || toolName === 'multiedit') && hasLspDiagnostics(cleaned)) {
+ cleaned = stripLspDiagnostics(cleaned);
+ }
+
+ if ((toolName === 'edit' || toolName === 'multiedit') && cleaned.trim().length === 0 && metadata?.diff) {
+
+ const diff = metadata.diff;
+ return typeof diff === 'string' ? diff : String(diff);
+ }
+
+ return cleaned;
+};
+
+export interface ParsedReadOutputLine {
+ text: string;
+ lineNumber: number | null;
+ isInfo: boolean;
+}
+
+export interface ParsedReadToolOutput {
+ type: 'file' | 'directory' | 'unknown';
+ lines: ParsedReadOutputLine[];
+}
+
+export const parseReadToolOutput = (output: string): ParsedReadToolOutput => {
+ const typeMatch = output.match(/(file|directory)<\/type>/i);
+ const detectedType = (typeMatch?.[1]?.toLowerCase() ?? 'unknown') as ParsedReadToolOutput['type'];
+
+ const contentMatch = output.match(/([\s\S]*?)<\/content>/i);
+ const rawContent = contentMatch?.[1] ?? output;
+ const normalizedContent = rawContent.replace(/\r\n/g, '\n');
+ const rawLines = normalizedContent.split('\n');
+
+ const isTruncationInfoLine = (text: string): boolean => {
+ return /\(\s*File has more lines\..*offset.*\)/i.test(text.trim());
+ };
+
+ const parsedLines = rawLines.map((line): ParsedReadOutputLine => {
+ const trimmed = line.trim();
+ const isInfo = (trimmed.startsWith('(') && trimmed.endsWith(')')) || isTruncationInfoLine(trimmed);
+
+ if (detectedType !== 'directory') {
+ const numberedMatch = line.match(/^(\d+):\s?(.*)$/);
+ if (numberedMatch) {
+ const numberedText = numberedMatch[2];
+ const numberedTrimmed = numberedText.trim();
+ const numberedIsInfo =
+ (numberedTrimmed.startsWith('(') && numberedTrimmed.endsWith(')'))
+ || isTruncationInfoLine(numberedTrimmed);
+ return {
+ lineNumber: numberedIsInfo ? null : Number(numberedMatch[1]),
+ text: numberedText,
+ isInfo: numberedIsInfo,
+ };
+ }
+ }
+
+ return {
+ lineNumber: null,
+ text: line,
+ isInfo,
+ };
+ });
+
+ const lines = parsedLines.filter((line, index, arr) => {
+ if (line.text.trim().length > 0) {
+ return true;
+ }
+
+ const prev = arr[index - 1];
+ const next = arr[index + 1];
+ const adjacentToInfo = Boolean(prev?.isInfo || next?.isInfo);
+ const hasNumber = line.lineNumber !== null;
+
+ // Drop numbered blank lines wrapped around helper/info rows.
+ if (adjacentToInfo && hasNumber) {
+ return false;
+ }
+
+ return true;
+ });
+
+ return {
+ type: detectedType,
+ lines,
+ };
+};
+
+export const renderListOutput = (output: string, options?: { unstyled?: boolean }) => {
+ try {
+ const lines = output.trim().split('\n').filter(Boolean);
+ if (lines.length === 0) return null;
+
+ const items: Array<{ name: string; depth: number; isFile: boolean }> = [];
+ lines.forEach((line) => {
+ const match = line.match(/^(\s*)(.+)$/);
+ if (match) {
+ const [, spaces, name] = match;
+ const depth = Math.floor(spaces.length / 2);
+ const isFile = !name.endsWith('/');
+ items.push({
+ name: name.replace(/\/$/, ''),
+ depth,
+ isFile,
+ });
+ }
+ });
+
+ return (
+
+ {items.map((item, idx) => (
+
+ {item.isFile ? (
+ {item.name}
+ ) : (
+ {item.name}/
+ )}
+
+ ))}
+
+ );
+ } catch {
+ return null;
+ }
+};
+
+export const renderGrepOutput = (output: string, isMobile: boolean, options?: { unstyled?: boolean }) => {
+ try {
+ const lines = output.trim().split('\n').filter(Boolean);
+ if (lines.length === 0) return null;
+
+ const fileGroups: Record> = {};
+
+ lines.forEach((line) => {
+ const match = line.match(/^(.+?):(\d+):(.*)$/) || line.match(/^(.+?):(.*)$/);
+ if (match) {
+ const [, filepath, lineNumOrContent, content] = match;
+ const lineNum = content !== undefined ? lineNumOrContent : '';
+ const actualContent = content !== undefined ? content : lineNumOrContent;
+
+ if (!fileGroups[filepath]) {
+ fileGroups[filepath] = [];
+ }
+ fileGroups[filepath].push({ lineNum, content: actualContent });
+ }
+ });
+
+ return (
+
+
+ Found {lines.length} match{lines.length !== 1 ? 'es' : ''}
+
+ {Object.entries(fileGroups).map(([filepath, matches]) => (
+
+
+ {filepath}
+
+
+ {matches.map((match, idx) => {
+ if (!match.lineNum && !match.content) {
+ return null;
+ }
+ return (
+
+
+
+ {match.lineNum && (
+
+ Line {match.lineNum}:
+
+ )}
+
+ {match.content || '\u00A0'}
+
+
+
+ );
+ })}
+
+
+ ))}
+
+ );
+
+ } catch {
+ return null;
+ }
+};
+
+export const renderGlobOutput = (output: string, isMobile: boolean, options?: { unstyled?: boolean }) => {
+ try {
+ const paths = output.trim().split('\n').filter(Boolean);
+ if (paths.length === 0) return null;
+
+ const groups: Record = {};
+ paths.forEach((path) => {
+ const lastSlash = path.lastIndexOf('/');
+ const dir = lastSlash > 0 ? path.substring(0, lastSlash) : '/';
+ const filename = lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
+
+ if (!groups[dir]) {
+ groups[dir] = [];
+ }
+ groups[dir].push(filename);
+ });
+
+ const sortedDirs = Object.keys(groups).sort();
+
+ return (
+
+
+ Found {paths.length} file{paths.length !== 1 ? 's' : ''}
+
+ {sortedDirs.map((dir) => (
+
+
+ {dir}/
+
+
+ {groups[dir].sort().map((filename) => (
+
+ ))}
+
+
+ ))}
+
+ );
+ } catch {
+ return null;
+ }
+};
+
+type Todo = {
+ id?: string;
+ content: string;
+ status: 'in_progress' | 'pending' | 'completed' | 'cancelled';
+ priority?: 'high' | 'medium' | 'low';
+};
+
+export const renderTodoOutput = (output: string, options?: { unstyled?: boolean }) => {
+ try {
+ const todos = JSON.parse(output) as Todo[];
+ if (!Array.isArray(todos)) {
+ return null;
+ }
+
+ const todosByStatus = {
+ in_progress: todos.filter((t) => t.status === 'in_progress'),
+ pending: todos.filter((t) => t.status === 'pending'),
+ completed: todos.filter((t) => t.status === 'completed'),
+ cancelled: todos.filter((t) => t.status === 'cancelled'),
+ };
+
+ const getPriorityDot = (priority?: string) => {
+ const baseClasses = 'w-2 h-2 rounded-full flex-shrink-0 mt-1';
+ switch (priority) {
+ case 'high':
+ return
;
+ case 'medium':
+ return
;
+ case 'low':
+ return
;
+ default:
+ return
;
+ }
+ };
+
+ return (
+
+
+ Total: {todos.length}
+ {todosByStatus.in_progress.length > 0 && (
+ In Progress: {todosByStatus.in_progress.length}
+ )}
+ {todosByStatus.pending.length > 0 && (
+ Pending: {todosByStatus.pending.length}
+ )}
+ {todosByStatus.completed.length > 0 && (
+ Completed: {todosByStatus.completed.length}
+ )}
+ {todosByStatus.cancelled.length > 0 && (
+ Cancelled: {todosByStatus.cancelled.length}
+ )}
+
+
+ {todosByStatus.in_progress.length > 0 && (
+
+
+
+ {todosByStatus.in_progress.map((todo, idx) => (
+
+ {getPriorityDot(todo.priority)}
+ {todo.content}
+
+ ))}
+
+
+ )}
+
+ {todosByStatus.pending.length > 0 && (
+
+
+
+ {todosByStatus.pending.map((todo, idx) => (
+
+ {getPriorityDot(todo.priority)}
+ {todo.content}
+
+ ))}
+
+
+ )}
+
+ {todosByStatus.completed.length > 0 && (
+
+
+
+ Completed
+
+
+ {todosByStatus.completed.map((todo, idx) => (
+
+
+ {todo.content}
+
+ ))}
+
+
+ )}
+
+ {todosByStatus.cancelled.length > 0 && (
+
+
+ ×
+ Cancelled
+
+
+ {todosByStatus.cancelled.map((todo, idx) => (
+
+ ×
+ {todo.content}
+
+ ))}
+
+
+ )}
+
+ );
+ } catch {
+ return null;
+ }
+};
+
+export const renderWebSearchOutput = (output: string, _syntaxTheme: { [key: string]: React.CSSProperties }, options?: { unstyled?: boolean }) => {
+ try {
+ return (
+
+
+
+ );
+ } catch {
+ return null;
+ }
+};
+
+export type DiffLineType = 'context' | 'added' | 'removed';
+
+export interface UnifiedDiffLine {
+ type: DiffLineType;
+ lineNumber: number | null;
+ content: string;
+}
+
+export interface UnifiedDiffHunk {
+ file: string;
+ oldStart: number;
+ newStart: number;
+ lines: UnifiedDiffLine[];
+}
+
+export interface SideBySideDiffLine {
+ leftLine: { type: 'context' | 'removed' | 'empty'; lineNumber: number | null; content: string };
+ rightLine: { type: 'context' | 'added' | 'empty'; lineNumber: number | null; content: string };
+}
+
+export interface SideBySideDiffHunk {
+ file: string;
+ oldStart: number;
+ newStart: number;
+ lines: SideBySideDiffLine[];
+}
+
+export const parseDiffToUnified = (diffText: string): UnifiedDiffHunk[] => {
+ const lines = diffText.split('\n');
+ let currentFile = '';
+ const hunks: UnifiedDiffHunk[] = [];
+
+ let i = 0;
+ while (i < lines.length) {
+ const line = lines[i];
+
+ if (line.startsWith('Index:') || line.startsWith('===') || line.startsWith('---') || line.startsWith('+++')) {
+ if (line.startsWith('Index:')) {
+ currentFile = line.split(' ')[1].split('/').pop() || 'file';
+ }
+ i++;
+ continue;
+ }
+
+ if (line.startsWith('@@')) {
+ const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
+ const oldStart = match ? parseInt(match[1]) : 0;
+ const newStart = match ? parseInt(match[2]) : 0;
+
+ const unifiedLines: UnifiedDiffLine[] = [];
+ let oldLineNum = oldStart;
+ let newLineNum = newStart;
+ let j = i + 1;
+
+ while (j < lines.length && !lines[j].startsWith('@@') && !lines[j].startsWith('Index:')) {
+ const contentLine = lines[j];
+ if (contentLine.startsWith('+')) {
+ unifiedLines.push({ type: 'added', lineNumber: newLineNum, content: contentLine.substring(1) });
+ newLineNum++;
+ } else if (contentLine.startsWith('-')) {
+ unifiedLines.push({ type: 'removed', lineNumber: oldLineNum, content: contentLine.substring(1) });
+ oldLineNum++;
+ } else if (contentLine.startsWith(' ')) {
+ unifiedLines.push({ type: 'context', lineNumber: newLineNum, content: contentLine.substring(1) });
+ oldLineNum++;
+ newLineNum++;
+ }
+ j++;
+ }
+
+ hunks.push({
+ file: currentFile,
+ oldStart,
+ newStart,
+ lines: unifiedLines,
+ });
+
+ i = j;
+ continue;
+ }
+
+ i++;
+ }
+
+ return hunks;
+};
+
+export const parseDiffToLines = (diffText: string): SideBySideDiffHunk[] => {
+ const lines = diffText.split('\n');
+ let currentFile = '';
+ const hunks: SideBySideDiffHunk[] = [];
+
+ let i = 0;
+ while (i < lines.length) {
+ const line = lines[i];
+
+ if (line.startsWith('Index:') || line.startsWith('===') || line.startsWith('---') || line.startsWith('+++')) {
+ if (line.startsWith('Index:')) {
+ currentFile = line.split(' ')[1].split('/').pop() || 'file';
+ }
+ i++;
+ continue;
+ }
+
+ if (line.startsWith('@@')) {
+ const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
+ const oldStart = match ? parseInt(match[1]) : 0;
+ const newStart = match ? parseInt(match[2]) : 0;
+
+ const changes: Array<{
+ type: 'context' | 'added' | 'removed';
+ content: string;
+ oldLine?: number;
+ newLine?: number;
+ }> = [];
+
+ let oldLineNum = oldStart;
+ let newLineNum = newStart;
+ let j = i + 1;
+
+ while (j < lines.length && !lines[j].startsWith('@@') && !lines[j].startsWith('Index:')) {
+ const contentLine = lines[j];
+ if (contentLine.startsWith('+')) {
+ changes.push({ type: 'added', content: contentLine.substring(1), newLine: newLineNum });
+ newLineNum++;
+ } else if (contentLine.startsWith('-')) {
+ changes.push({ type: 'removed', content: contentLine.substring(1), oldLine: oldLineNum });
+ oldLineNum++;
+ } else if (contentLine.startsWith(' ')) {
+ changes.push({
+ type: 'context',
+ content: contentLine.substring(1),
+ oldLine: oldLineNum,
+ newLine: newLineNum,
+ });
+ oldLineNum++;
+ newLineNum++;
+ }
+ j++;
+ }
+
+ const alignedLines: Array<{
+ leftLine: { type: 'context' | 'removed' | 'empty'; lineNumber: number | null; content: string };
+ rightLine: { type: 'context' | 'added' | 'empty'; lineNumber: number | null; content: string };
+ }> = [];
+
+ const leftSide: Array<{ type: 'context' | 'removed'; lineNumber: number; content: string }> = [];
+ const rightSide: Array<{ type: 'context' | 'added'; lineNumber: number; content: string }> = [];
+
+ changes.forEach((change) => {
+ if (change.type === 'context') {
+ leftSide.push({ type: 'context', lineNumber: change.oldLine!, content: change.content });
+ rightSide.push({ type: 'context', lineNumber: change.newLine!, content: change.content });
+ } else if (change.type === 'removed') {
+ leftSide.push({ type: 'removed', lineNumber: change.oldLine!, content: change.content });
+ } else if (change.type === 'added') {
+ rightSide.push({ type: 'added', lineNumber: change.newLine!, content: change.content });
+ }
+ });
+
+ const alignmentPoints: Array<{ leftIdx: number; rightIdx: number }> = [];
+
+ leftSide.forEach((leftItem, leftIdx) => {
+ if (leftItem.type === 'context') {
+ const rightIdx = rightSide.findIndex((rightItem, rIdx) =>
+ rightItem.type === 'context' &&
+ rightItem.content === leftItem.content &&
+ !alignmentPoints.some((ap) => ap.rightIdx === rIdx)
+ );
+ if (rightIdx >= 0) {
+ alignmentPoints.push({ leftIdx, rightIdx });
+ }
+ }
+ });
+
+ alignmentPoints.sort((a, b) => a.leftIdx - b.leftIdx);
+
+ let leftIdx = 0;
+ let rightIdx = 0;
+ let alignIdx = 0;
+
+ while (leftIdx < leftSide.length || rightIdx < rightSide.length) {
+ const nextAlign = alignIdx < alignmentPoints.length ? alignmentPoints[alignIdx] : null;
+
+ if (nextAlign && leftIdx === nextAlign.leftIdx && rightIdx === nextAlign.rightIdx) {
+ const leftItem = leftSide[leftIdx];
+ const rightItem = rightSide[rightIdx];
+
+ alignedLines.push({
+ leftLine: {
+ type: 'context',
+ lineNumber: leftItem.lineNumber,
+ content: leftItem.content,
+ },
+ rightLine: {
+ type: 'context',
+ lineNumber: rightItem.lineNumber,
+ content: rightItem.content,
+ },
+ });
+
+ leftIdx++;
+ rightIdx++;
+ alignIdx++;
+ } else {
+ const needProcessLeft = leftIdx < leftSide.length && (!nextAlign || leftIdx < nextAlign.leftIdx);
+ const needProcessRight = rightIdx < rightSide.length && (!nextAlign || rightIdx < nextAlign.rightIdx);
+
+ if (needProcessLeft && needProcessRight) {
+ const leftItem = leftSide[leftIdx];
+ const rightItem = rightSide[rightIdx];
+
+ alignedLines.push({
+ leftLine: {
+ type: leftItem.type,
+ lineNumber: leftItem.lineNumber,
+ content: leftItem.content,
+ },
+ rightLine: {
+ type: rightItem.type,
+ lineNumber: rightItem.lineNumber,
+ content: rightItem.content,
+ },
+ });
+
+ leftIdx++;
+ rightIdx++;
+ } else if (needProcessLeft) {
+ const leftItem = leftSide[leftIdx];
+ alignedLines.push({
+ leftLine: {
+ type: leftItem.type,
+ lineNumber: leftItem.lineNumber,
+ content: leftItem.content,
+ },
+ rightLine: {
+ type: 'empty',
+ lineNumber: null,
+ content: '',
+ },
+ });
+ leftIdx++;
+ } else if (needProcessRight) {
+ const rightItem = rightSide[rightIdx];
+ alignedLines.push({
+ leftLine: {
+ type: 'empty',
+ lineNumber: null,
+ content: '',
+ },
+ rightLine: {
+ type: rightItem.type,
+ lineNumber: rightItem.lineNumber,
+ content: rightItem.content,
+ },
+ });
+ rightIdx++;
+ } else {
+ break;
+ }
+ }
+ }
+
+ hunks.push({
+ file: currentFile,
+ oldStart,
+ newStart,
+ lines: alignedLines,
+ });
+
+ i = j;
+ continue;
+ }
+
+ i++;
+ }
+
+ return hunks;
+};
+
+export const detectLanguageFromOutput = (output: string, toolName: string, input?: Record) => {
+ return detectToolOutputLanguage(toolName, output, input);
+};
+
+export { formatInputForDisplay };
diff --git a/ui/src/components/chat/message/types.ts b/ui/src/components/chat/message/types.ts
new file mode 100644
index 0000000..ba9e3b4
--- /dev/null
+++ b/ui/src/components/chat/message/types.ts
@@ -0,0 +1,37 @@
+export type StreamPhase = 'streaming' | 'cooldown' | 'completed';
+
+export type DiffViewMode = 'side-by-side' | 'unified';
+
+export interface AgentMentionInfo {
+ name: string;
+ token: string;
+}
+
+export interface ToolPopupContent {
+ open: boolean;
+ title: string;
+ content: string;
+ language?: string;
+ isDiff?: boolean;
+ diffHunks?: Array>;
+ metadata?: Record;
+ image?: {
+ url: string;
+ mimeType?: string;
+ filename?: string;
+ size?: number;
+ gallery?: Array<{
+ url: string;
+ mimeType?: string;
+ filename?: string;
+ size?: number;
+ }>;
+ index?: number;
+ };
+ mermaid?: {
+ url: string;
+ mimeType?: string;
+ filename?: string;
+ source?: string;
+ };
+}
diff --git a/ui/src/components/chat/mobileControlsUtils.ts b/ui/src/components/chat/mobileControlsUtils.ts
new file mode 100644
index 0000000..f262d4e
--- /dev/null
+++ b/ui/src/components/chat/mobileControlsUtils.ts
@@ -0,0 +1,105 @@
+import type { Agent } from '@opencode-ai/sdk/v2';
+
+export type MobileControlsPanel = 'model' | 'agent' | 'variant' | null;
+
+export const isPrimaryMode = (mode?: string) => mode === 'primary' || mode === 'all' || mode === undefined || mode === null;
+
+export const capitalizeLabel = (value: string) => value.charAt(0).toUpperCase() + value.slice(1);
+
+export const getAgentDisplayName = (agents: Agent[], agentName?: string) => {
+ if (agentName) {
+ const agent = agents.find((entry) => entry.name === agentName);
+ return agent ? capitalizeLabel(agent.name) : capitalizeLabel(agentName);
+ }
+
+ const primaryAgents = agents.filter((agent) => isPrimaryMode(agent.mode));
+ const buildAgent = primaryAgents.find((agent) => agent.name === 'build');
+ const fallbackAgent = buildAgent || primaryAgents[0] || agents[0];
+ return fallbackAgent ? capitalizeLabel(fallbackAgent.name) : 'Select agent';
+};
+
+type ProviderModel = { id?: string; name?: string };
+
+export const getModelDisplayName = (
+ provider: { models?: ProviderModel[] } | undefined,
+ modelId: string | undefined,
+) => {
+ if (!provider || !modelId) {
+ return 'Not selected';
+ }
+ const models = Array.isArray(provider.models) ? provider.models : [];
+ const model = models.find((entry) => entry.id === modelId);
+ if (typeof model?.name === 'string' && model.name.trim().length > 0) {
+ return model.name;
+ }
+ if (typeof model?.id === 'string' && model.id.trim().length > 0) {
+ return model.id;
+ }
+ return modelId;
+};
+
+export const formatEffortLabel = (variant?: string) => {
+ if (!variant || variant.trim().length === 0) {
+ return 'Default';
+ }
+ const trimmed = variant.trim();
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
+ return trimmed;
+ }
+ return capitalizeLabel(trimmed);
+};
+
+export const DEFAULT_EFFORT_KEY = 'default';
+
+export const serializeEffortVariant = (variant?: string) => {
+ const trimmed = typeof variant === 'string' ? variant.trim() : '';
+ return trimmed.length > 0 ? trimmed : DEFAULT_EFFORT_KEY;
+};
+
+export const parseEffortVariant = (variant: string) => {
+ return variant === DEFAULT_EFFORT_KEY ? undefined : variant;
+};
+
+const EFFORT_RANKS: Record = {
+ max: 6,
+ maximum: 6,
+ xhigh: 5,
+ high: 4,
+ medium: 3,
+ default: 2,
+ low: 1,
+ min: 0,
+ minimal: 0,
+};
+
+export const getEffortRank = (variant?: string) => {
+ if (!variant || variant.trim().length === 0) {
+ return EFFORT_RANKS.default;
+ }
+ const normalized = variant.trim().toLowerCase();
+ if (Object.prototype.hasOwnProperty.call(EFFORT_RANKS, normalized)) {
+ return EFFORT_RANKS[normalized];
+ }
+ const numeric = Number.parseFloat(normalized);
+ return Number.isFinite(numeric) ? numeric : 0;
+};
+
+export const getQuickEffortOptions = (variants: string[]) => {
+ const options = new Map();
+ options.set('default', undefined);
+ for (const variant of variants) {
+ options.set(variant, variant);
+ }
+
+ const ordered = Array.from(options.values()).sort((a, b) => getEffortRank(b) - getEffortRank(a));
+ if (ordered.length <= 4) {
+ return ordered;
+ }
+
+ const top = ordered.slice(0, 3);
+ const lowest = ordered[ordered.length - 1];
+ if (top.some((item) => item === lowest)) {
+ return top;
+ }
+ return [...top, lowest];
+};
diff --git a/ui/src/components/comments/CodeMirrorCommentWidgets.tsx b/ui/src/components/comments/CodeMirrorCommentWidgets.tsx
new file mode 100644
index 0000000..e697182
--- /dev/null
+++ b/ui/src/components/comments/CodeMirrorCommentWidgets.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import { InlineCommentCard } from './InlineCommentCard';
+import { InlineCommentInput } from './InlineCommentInput';
+import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
+import type { BlockWidgetDef } from '@/components/ui/CodeMirrorEditor';
+
+type LineRange = {
+ start: number;
+ end: number;
+ side?: 'additions' | 'deletions';
+};
+
+interface CodeMirrorCommentWidgetsOptions {
+ drafts: InlineCommentDraft[];
+ editingDraftId: string | null;
+ commentText: string;
+ selection: LineRange | null;
+ isDragging: boolean;
+ fileLabel: string;
+ newWidgetId: string;
+ mapDraftToRange: (draft: InlineCommentDraft) => LineRange;
+ onSave: (text: string, range?: LineRange) => void;
+ onCancel: () => void;
+ onEdit: (draft: InlineCommentDraft) => void;
+ onDelete: (draft: InlineCommentDraft) => void;
+}
+
+export function buildCodeMirrorCommentWidgets(options: CodeMirrorCommentWidgetsOptions): BlockWidgetDef[] {
+ const {
+ drafts,
+ editingDraftId,
+ commentText,
+ selection,
+ isDragging,
+ fileLabel,
+ newWidgetId,
+ mapDraftToRange,
+ onSave,
+ onCancel,
+ onEdit,
+ onDelete,
+ } = options;
+
+ const widgets: BlockWidgetDef[] = [];
+
+ for (const draft of drafts) {
+ const draftRange = mapDraftToRange(draft);
+ if (draft.id === editingDraftId) {
+ widgets.push({
+ afterLine: draftRange.end,
+ id: `edit-${draft.id}`,
+ content: (
+
+ ),
+ });
+ continue;
+ }
+
+ widgets.push({
+ afterLine: draftRange.end,
+ id: `card-${draft.id}`,
+ content: (
+ onEdit(draft)}
+ onDelete={() => onDelete(draft)}
+ />
+ ),
+ });
+ }
+
+ if (selection && !editingDraftId && !isDragging) {
+ const normalizedSelection = {
+ ...selection,
+ start: Math.min(selection.start, selection.end),
+ end: Math.max(selection.start, selection.end),
+ };
+
+ widgets.push({
+ afterLine: normalizedSelection.end,
+ id: newWidgetId,
+ content: (
+
+ ),
+ });
+ }
+
+ return widgets;
+}
diff --git a/ui/src/components/comments/InlineCommentCard.tsx b/ui/src/components/comments/InlineCommentCard.tsx
new file mode 100644
index 0000000..2ca822d
--- /dev/null
+++ b/ui/src/components/comments/InlineCommentCard.tsx
@@ -0,0 +1,119 @@
+import React, { useState } from 'react';
+import { RiMoreLine, RiDeleteBinLine, RiEditLine, RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react';
+import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
+import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
+
+interface InlineCommentCardProps {
+ draft: InlineCommentDraft;
+ onEdit: () => void;
+ onDelete: () => void;
+ className?: string;
+ maxWidth?: number;
+}
+
+export function InlineCommentCard({
+ draft,
+ onEdit,
+ onDelete,
+ className,
+ maxWidth,
+}: InlineCommentCardProps) {
+ const themeContext = useOptionalThemeSystem();
+ const currentTheme = themeContext?.currentTheme;
+ const [isOpen, setIsOpen] = useState(false);
+
+ // Check if content is long enough to warrant collapsing (rough estimate)
+ // In a real app we might measure line height, but length check is a good proxy for now
+ const isLongContent = draft.text.length > 150 || draft.text.split('\n').length > 3;
+
+ return (
+
+
+
+
+
+ {draft.fileLabel}
+
+ •
+ Lines {draft.startLine}-{draft.endLine}
+ {draft.side && ({draft.side}) }
+
+
+
+
+ {draft.text}
+
+
+ {isLongContent && (
+
+
+ {isOpen ? (
+ <>
+
+ Show less
+ >
+ ) : (
+ <>
+
+ Show more
+ >
+ )}
+
+
+ )}
+
+
+ {/* Used for animation purposes if we want to animate height */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit comment
+
+
+
+ Delete comment
+
+
+
+
+
+ );
+}
diff --git a/ui/src/components/comments/InlineCommentInput.tsx b/ui/src/components/comments/InlineCommentInput.tsx
new file mode 100644
index 0000000..5a93cd5
--- /dev/null
+++ b/ui/src/components/comments/InlineCommentInput.tsx
@@ -0,0 +1,179 @@
+import React, { useRef, useEffect } from 'react';
+import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
+import { Button } from '@/components/ui/button';
+import { Textarea } from '@/components/ui/textarea';
+import { cn } from '@/lib/utils';
+import { useDeviceInfo } from '@/lib/device';
+
+export interface InlineCommentInputProps {
+ initialText?: string;
+ onSave: (text: string, range?: { start: number; end: number; side?: 'additions' | 'deletions' }) => void;
+ onCancel: () => void;
+ fileLabel?: string;
+ lineRange?: { start: number; end: number; side?: 'additions' | 'deletions' };
+ isEditing?: boolean;
+ className?: string;
+ maxWidth?: number;
+}
+
+export function InlineCommentInput({
+ initialText = '',
+ onSave,
+ onCancel,
+ fileLabel,
+ lineRange,
+ isEditing = false,
+ className,
+ maxWidth,
+}: InlineCommentInputProps) {
+ const themeContext = useOptionalThemeSystem();
+ const currentTheme = themeContext?.currentTheme;
+ const { isMobile } = useDeviceInfo();
+ const [text, setText] = React.useState(initialText);
+ const textareaRef = useRef(null);
+
+ // Stable range snapshot to prevent race with selection clearing
+ const stableRangeRef = useRef(lineRange);
+ useEffect(() => {
+ if (lineRange) {
+ stableRangeRef.current = lineRange;
+ }
+ }, [lineRange]);
+
+ const normalizeRange = (range?: { start: number; end: number; side?: 'additions' | 'deletions' }) => {
+ if (!range) return undefined;
+ const start = Math.min(range.start, range.end);
+ const end = Math.max(range.start, range.end);
+ return { ...range, start, end };
+ };
+
+ const displayRange = normalizeRange(lineRange);
+
+ // Focus on mount (desktop only) or when becoming visible
+ useEffect(() => {
+ const textarea = textareaRef.current;
+ if (!textarea) return;
+
+ const scrollContainer = textarea.closest('.overlay-scrollbar-container') as HTMLElement | null;
+ const prevScrollTop = scrollContainer?.scrollTop ?? window.scrollY;
+ const prevScrollLeft = scrollContainer?.scrollLeft ?? window.scrollX;
+
+ if (isMobile) {
+ textarea.scrollIntoView({ behavior: 'auto', block: 'nearest' });
+ try {
+ textarea.focus({ preventScroll: true });
+ } catch {
+ textarea.focus();
+ }
+ return;
+ }
+
+ try {
+ textarea.focus({ preventScroll: true });
+ } catch {
+ textarea.focus();
+ }
+
+ const len = textarea.value.length;
+ try {
+ textarea.setSelectionRange(len, len);
+ } catch (err) {
+ void err;
+ }
+
+ requestAnimationFrame(() => {
+ if (scrollContainer) {
+ scrollContainer.scrollTop = prevScrollTop;
+ scrollContainer.scrollLeft = prevScrollLeft;
+ } else {
+ window.scrollTo({ top: prevScrollTop, left: prevScrollLeft });
+ }
+ });
+ }, [isMobile]);
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
+ e.preventDefault();
+ if (text.trim()) {
+ onSave(text, normalizeRange(stableRangeRef.current));
+ }
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ onCancel();
+ }
+ };
+
+ const handleSaveClick = (e: React.MouseEvent | React.TouchEvent | React.PointerEvent) => {
+ // Stop propagation to prevent parent selection clearing before save
+ e.stopPropagation();
+ if (text.trim()) {
+ onSave(text, normalizeRange(stableRangeRef.current));
+ }
+ };
+
+ return (
+ e.stopPropagation()}
+ onTouchStart={(e) => e.stopPropagation()}
+ >
+
+ {(fileLabel || lineRange) && (
+
+ {fileLabel && {fileLabel} }
+ {fileLabel && lineRange && • }
+ {displayRange && Lines {displayRange.start}-{displayRange.end} }
+
+ )}
+
+
setText(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Add a comment... (Cmd+Enter to save)"
+ className="min-h-[80px] text-sm resize-y"
+ style={{
+ backgroundColor: currentTheme?.colors?.surface?.subtle,
+ }}
+ />
+
+
+ e.stopPropagation()}
+ onTouchStart={(e) => e.stopPropagation()}
+ className="h-8 text-muted-foreground hover:text-foreground"
+ >
+ Cancel
+
+ e.stopPropagation()}
+ onTouchStart={(e) => e.stopPropagation()}
+ disabled={!text.trim()}
+ className="h-8 min-w-[80px]"
+ style={{
+ backgroundColor: currentTheme?.colors?.status?.success,
+ color: currentTheme?.colors?.status?.successForeground,
+ }}
+ >
+ {isEditing ? 'Save' : 'Comment'}
+
+
+
+
+ );
+}
diff --git a/ui/src/components/comments/PierreDiffCommentOverlays.tsx b/ui/src/components/comments/PierreDiffCommentOverlays.tsx
new file mode 100644
index 0000000..73b8d1d
--- /dev/null
+++ b/ui/src/components/comments/PierreDiffCommentOverlays.tsx
@@ -0,0 +1,230 @@
+import React from 'react';
+import { createPortal } from 'react-dom';
+import type { SelectedLineRange } from '@pierre/diffs';
+import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
+import { InlineCommentCard } from './InlineCommentCard';
+import { InlineCommentInput } from './InlineCommentInput';
+import { toPierreAnnotationId } from './PierreDiffCommentUtils';
+
+interface PierreDiffCommentOverlaysProps {
+ diffRootRef: React.RefObject;
+ drafts: InlineCommentDraft[];
+ selection: SelectedLineRange | null;
+ editingDraftId: string | null;
+ commentText: string;
+ fileLabel: string;
+ onSave: (text: string, range?: SelectedLineRange) => void;
+ onCancel: () => void;
+ onEdit: (draft: InlineCommentDraft) => void;
+ onDelete: (draft: InlineCommentDraft) => void;
+}
+
+function parseCssWidth(value: string): number | null {
+ const trimmed = value.trim();
+ if (!trimmed) return null;
+
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
+ const parsed = Number.parseFloat(trimmed);
+ return Number.isFinite(parsed) ? parsed : null;
+ }
+
+ if (trimmed.endsWith('px')) {
+ const parsed = Number.parseFloat(trimmed.slice(0, -2));
+ return Number.isFinite(parsed) ? parsed : null;
+ }
+
+ return null;
+}
+
+function clampMaxWidth(value: number | null | undefined): number | undefined {
+ if (!value || value <= 0) return undefined;
+ return Math.max(200, Math.floor(value));
+}
+
+export function PierreDiffCommentOverlays(props: PierreDiffCommentOverlaysProps) {
+ const {
+ diffRootRef,
+ drafts,
+ selection,
+ editingDraftId,
+ commentText,
+ fileLabel,
+ onSave,
+ onCancel,
+ onEdit,
+ onDelete,
+ } = props;
+
+ const [retryTick, setRetryTick] = React.useState(0);
+ const [fallbackMaxWidth, setFallbackMaxWidth] = React.useState(null);
+
+ const selectionAnnotationId = React.useMemo(() => {
+ if (!selection || editingDraftId) return null;
+ return toPierreAnnotationId({ type: 'new', selection });
+ }, [editingDraftId, selection]);
+
+ const expectedTargetIds = React.useMemo(() => {
+ const ids = drafts.map((draft) => toPierreAnnotationId({ type: draft.id === editingDraftId ? 'edit' : 'saved', draft }));
+ if (selectionAnnotationId) {
+ ids.push(selectionAnnotationId);
+ }
+ return ids;
+ }, [drafts, editingDraftId, selectionAnnotationId]);
+
+ const resolveTarget = React.useCallback((annotationId: string): HTMLElement | null => {
+ const wrapper = diffRootRef.current;
+ if (!wrapper) return null;
+
+ const host = wrapper.querySelector('diffs-container');
+ if (!(host instanceof HTMLElement)) return null;
+
+ const lightDomTarget = host.querySelector(`[data-annotation-id="${annotationId}"]`);
+ if (lightDomTarget instanceof HTMLElement) {
+ return lightDomTarget;
+ }
+
+ const shadowRoot = host.shadowRoot;
+ if (!shadowRoot) return null;
+
+ return shadowRoot.querySelector(`[data-annotation-id="${annotationId}"]`) as HTMLElement | null;
+ }, [diffRootRef]);
+
+ React.useEffect(() => {
+ if (expectedTargetIds.length === 0) return;
+
+ let cancelled = false;
+ let attempts = 0;
+ const maxAttempts = 12;
+
+ const checkTargets = () => {
+ if (cancelled) return;
+ const allResolved = expectedTargetIds.every((id) => Boolean(resolveTarget(id)));
+ if (allResolved || attempts >= maxAttempts) {
+ return;
+ }
+ attempts += 1;
+ requestAnimationFrame(() => {
+ if (cancelled) return;
+ setRetryTick((tick) => tick + 1);
+ checkTargets();
+ });
+ };
+
+ checkTargets();
+ return () => {
+ cancelled = true;
+ };
+ }, [expectedTargetIds, resolveTarget]);
+
+ React.useEffect(() => {
+ const root = diffRootRef.current;
+ if (!root) return;
+
+ const computeMaxWidth = () => {
+ const styles = getComputedStyle(root);
+ const cssWidth = parseCssWidth(styles.getPropertyValue('--oc-context-panel-width'));
+ const rootRect = root.getBoundingClientRect();
+ const measured = cssWidth ?? rootRect.width;
+ setFallbackMaxWidth(measured > 0 ? measured : null);
+ };
+
+ computeMaxWidth();
+
+ const observer = new ResizeObserver(() => {
+ computeMaxWidth();
+ });
+ observer.observe(root);
+
+ window.addEventListener('resize', computeMaxWidth);
+ return () => {
+ observer.disconnect();
+ window.removeEventListener('resize', computeMaxWidth);
+ };
+ }, [diffRootRef]);
+
+ const resolveTargetMaxWidth = React.useCallback((target: HTMLElement): number | undefined => {
+ const root = diffRootRef.current;
+ const rootRect = root?.getBoundingClientRect();
+
+ const annotationContent = target.closest('[data-annotation-content]');
+ const contentRect = annotationContent instanceof HTMLElement
+ ? annotationContent.getBoundingClientRect()
+ : target.getBoundingClientRect();
+
+ const candidates = [contentRect.width];
+ if (rootRect) {
+ candidates.push(rootRect.right - contentRect.left);
+ }
+
+ const positiveCandidates = candidates.filter((value) => Number.isFinite(value) && value > 0);
+ if (positiveCandidates.length > 0) {
+ return clampMaxWidth(Math.min(...positiveCandidates));
+ }
+
+ return clampMaxWidth(fallbackMaxWidth);
+ }, [diffRootRef, fallbackMaxWidth]);
+
+ void retryTick;
+
+ return (
+ <>
+ {drafts.map((draft) => {
+ const id = toPierreAnnotationId({ type: draft.id === editingDraftId ? 'edit' : 'saved', draft });
+ const target = resolveTarget(id);
+ if (!target) return null;
+ const targetMaxWidth = resolveTargetMaxWidth(target);
+
+ if (draft.id === editingDraftId) {
+ return createPortal(
+ ,
+ target,
+ `draft-edit-${draft.id}`
+ );
+ }
+
+ return createPortal(
+ onEdit(draft)}
+ onDelete={() => onDelete(draft)}
+ maxWidth={targetMaxWidth}
+ />,
+ target,
+ `draft-card-${draft.id}`
+ );
+ })}
+
+ {selection && !editingDraftId && selectionAnnotationId && (() => {
+ const target = resolveTarget(selectionAnnotationId);
+ if (!target) return null;
+ const targetMaxWidth = resolveTargetMaxWidth(target);
+
+ return createPortal(
+ ,
+ target,
+ selectionAnnotationId
+ );
+ })()}
+ >
+ );
+}
diff --git a/ui/src/components/comments/PierreDiffCommentUtils.ts b/ui/src/components/comments/PierreDiffCommentUtils.ts
new file mode 100644
index 0000000..df1927d
--- /dev/null
+++ b/ui/src/components/comments/PierreDiffCommentUtils.ts
@@ -0,0 +1,52 @@
+import type { AnnotationSide, DiffLineAnnotation, SelectedLineRange } from '@pierre/diffs';
+import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
+
+export type PierreAnnotationData =
+ | { type: 'saved' | 'edit'; draft: InlineCommentDraft }
+ | { type: 'new'; selection: SelectedLineRange };
+
+export const toPierreAnnotationId = (meta: PierreAnnotationData): string => {
+ if (meta.type === 'new') {
+ const start = Math.min(meta.selection.start, meta.selection.end);
+ const end = Math.max(meta.selection.start, meta.selection.end);
+ const side = meta.selection.side ?? 'additions';
+ return `new-comment-${side}-${start}-${end}`;
+ }
+
+ return `draft-${meta.draft.id}`;
+};
+
+interface BuildPierreLineAnnotationsOptions {
+ drafts: InlineCommentDraft[];
+ editingDraftId: string | null;
+ selection: SelectedLineRange | null;
+}
+
+export const buildPierreLineAnnotations = (
+ options: BuildPierreLineAnnotationsOptions
+): DiffLineAnnotation[] => {
+ const { drafts, editingDraftId, selection } = options;
+ const annotations: DiffLineAnnotation[] = [];
+
+ for (const draft of drafts) {
+ const side: AnnotationSide = draft.side === 'original' ? 'deletions' : 'additions';
+ annotations.push({
+ lineNumber: draft.endLine,
+ side,
+ metadata: {
+ type: draft.id === editingDraftId ? 'edit' : 'saved',
+ draft,
+ },
+ });
+ }
+
+ if (selection && !editingDraftId) {
+ annotations.push({
+ lineNumber: Math.max(selection.start, selection.end),
+ side: selection.side ?? 'additions',
+ metadata: { type: 'new', selection },
+ });
+ }
+
+ return annotations;
+};
diff --git a/ui/src/components/comments/index.ts b/ui/src/components/comments/index.ts
new file mode 100644
index 0000000..264f8db
--- /dev/null
+++ b/ui/src/components/comments/index.ts
@@ -0,0 +1,6 @@
+export * from './InlineCommentCard';
+export * from './InlineCommentInput';
+export * from './useInlineCommentController';
+export * from './CodeMirrorCommentWidgets';
+export * from './PierreDiffCommentUtils';
+export * from './PierreDiffCommentOverlays';
diff --git a/ui/src/components/comments/useInlineCommentController.ts b/ui/src/components/comments/useInlineCommentController.ts
new file mode 100644
index 0000000..e70cbd5
--- /dev/null
+++ b/ui/src/components/comments/useInlineCommentController.ts
@@ -0,0 +1,154 @@
+import React from 'react';
+import { toast } from '@/components/ui';
+import { useInlineCommentDraftStore, type InlineCommentDraft, type InlineCommentSource } from '@/stores/useInlineCommentDraftStore';
+import { useSessionStore } from '@/stores/useSessionStore';
+
+type LineRangeBase = {
+ start: number;
+ end: number;
+};
+
+type StoreRange = {
+ startLine: number;
+ endLine: number;
+ side?: 'original' | 'modified';
+};
+
+interface UseInlineCommentControllerOptions {
+ source: InlineCommentSource;
+ fileLabel: string | null;
+ language: string;
+ getCodeForRange: (range: TRange) => string;
+ toStoreRange: (range: TRange) => StoreRange;
+ fromDraftRange: (draft: InlineCommentDraft) => TRange;
+}
+
+const normalizeStoreRange = (range: StoreRange): StoreRange => {
+ const startLine = Math.min(range.startLine, range.endLine);
+ const endLine = Math.max(range.startLine, range.endLine);
+ return {
+ ...range,
+ startLine,
+ endLine,
+ };
+};
+
+export const normalizeLineRange = (range: TRange): TRange => {
+ const start = Math.min(range.start, range.end);
+ const end = Math.max(range.start, range.end);
+ return {
+ ...range,
+ start,
+ end,
+ };
+};
+
+export function useInlineCommentController(
+ options: UseInlineCommentControllerOptions
+) {
+ const { source, fileLabel, language, getCodeForRange, toStoreRange, fromDraftRange } = options;
+
+ const currentSessionId = useSessionStore((state) => state.currentSessionId);
+ const newSessionDraftOpen = useSessionStore((state) => state.newSessionDraft?.open);
+
+ const addDraft = useInlineCommentDraftStore((state) => state.addDraft);
+ const updateDraft = useInlineCommentDraftStore((state) => state.updateDraft);
+ const removeDraft = useInlineCommentDraftStore((state) => state.removeDraft);
+ const allDrafts = useInlineCommentDraftStore((state) => state.drafts);
+
+ const [selection, setSelection] = React.useState(null);
+ const [commentText, setCommentText] = React.useState('');
+ const [editingDraftId, setEditingDraftId] = React.useState(null);
+
+ const sessionKey = React.useMemo(() => {
+ return currentSessionId ?? (newSessionDraftOpen ? 'draft' : null);
+ }, [currentSessionId, newSessionDraftOpen]);
+
+ const drafts = React.useMemo(() => {
+ if (!sessionKey || !fileLabel) return [];
+ const sessionDrafts = allDrafts[sessionKey] ?? [];
+ return sessionDrafts.filter((draft) => draft.source === source && draft.fileLabel === fileLabel);
+ }, [allDrafts, fileLabel, sessionKey, source]);
+
+ const reset = React.useCallback(() => {
+ setSelection(null);
+ setCommentText('');
+ setEditingDraftId(null);
+ }, []);
+
+ const cancel = React.useCallback(() => {
+ reset();
+ }, [reset]);
+
+ const startEdit = React.useCallback((draft: InlineCommentDraft) => {
+ const draftRange = normalizeLineRange(fromDraftRange(draft));
+ setSelection(draftRange);
+ setCommentText(draft.text);
+ setEditingDraftId(draft.id);
+ }, [fromDraftRange]);
+
+ const deleteDraft = React.useCallback((draft: InlineCommentDraft) => {
+ removeDraft(draft.sessionKey, draft.id);
+ if (editingDraftId === draft.id) {
+ reset();
+ }
+ }, [editingDraftId, removeDraft, reset]);
+
+ const saveComment = React.useCallback((textToSave: string, rangeOverride?: TRange) => {
+ const targetRange = rangeOverride ?? selection;
+ const trimmedText = textToSave.trim();
+ if (!targetRange || !trimmedText || !fileLabel) return;
+
+ if (!sessionKey) {
+ toast.error('Select a session to save comment');
+ return;
+ }
+
+ const normalizedRange = normalizeLineRange(targetRange);
+ const normalizedStoreRange = normalizeStoreRange(toStoreRange(normalizedRange));
+ const code = getCodeForRange(normalizedRange);
+
+ if (editingDraftId) {
+ updateDraft(sessionKey, editingDraftId, {
+ fileLabel,
+ startLine: normalizedStoreRange.startLine,
+ endLine: normalizedStoreRange.endLine,
+ side: normalizedStoreRange.side,
+ code,
+ language,
+ text: trimmedText,
+ });
+ } else {
+ addDraft({
+ sessionKey,
+ source,
+ fileLabel,
+ startLine: normalizedStoreRange.startLine,
+ endLine: normalizedStoreRange.endLine,
+ side: normalizedStoreRange.side,
+ code,
+ language,
+ text: trimmedText,
+ });
+ }
+
+ reset();
+ }, [addDraft, editingDraftId, fileLabel, getCodeForRange, language, reset, selection, sessionKey, source, toStoreRange, updateDraft]);
+
+ return {
+ sessionKey,
+ drafts,
+ selection,
+ setSelection,
+ commentText,
+ setCommentText,
+ editingDraftId,
+ setEditingDraftId,
+ reset,
+ cancel,
+ startEdit,
+ deleteDraft,
+ saveComment,
+ fromDraftRange,
+ };
+}
diff --git a/ui/src/components/desktop/DesktopHostSwitcher.tsx b/ui/src/components/desktop/DesktopHostSwitcher.tsx
new file mode 100644
index 0000000..b24c791
--- /dev/null
+++ b/ui/src/components/desktop/DesktopHostSwitcher.tsx
@@ -0,0 +1,1389 @@
+import * as React from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ RiAddLine,
+ RiCheckLine,
+ RiCloudOffLine,
+ RiEarthLine,
+ RiLoader4Line,
+ RiMore2Line,
+ RiPencilLine,
+ RiPlug2Line,
+ RiRefreshLine,
+ RiServerLine,
+ RiSettings3Line,
+ RiShieldKeyholeLine,
+ RiStarFill,
+ RiStarLine,
+ RiDeleteBinLine,
+ RiWindowLine,
+} from '@remixicon/react';
+import { cn } from '@/lib/utils';
+import { toast } from '@/components/ui';
+import { isTauriShell, isDesktopShell } from '@/lib/desktop';
+import { useUIStore } from '@/stores/useUIStore';
+import {
+ desktopHostProbe,
+ desktopHostsGet,
+ desktopHostsSet,
+ desktopOpenNewWindowAtUrl,
+ locationMatchesHost,
+ normalizeHostUrl,
+ redactSensitiveUrl,
+ type DesktopHost,
+ type HostProbeResult,
+} from '@/lib/desktopHosts';
+import {
+ desktopSshConnect,
+ desktopSshDisconnect,
+ desktopSshInstancesGet,
+ desktopSshStatus,
+ type DesktopSshInstanceStatus,
+} from '@/lib/desktopSsh';
+
+const LOCAL_HOST_ID = 'local';
+const SSH_CONNECT_TIMEOUT_MS = 90_000;
+const SSH_CONNECT_CANCELLED_ERROR = 'SSH connection cancelled';
+
+type HostStatus = {
+ status: HostProbeResult['status'];
+ latencyMs: number;
+};
+
+const toNavigationUrl = (rawUrl: string): string => {
+ const normalized = normalizeHostUrl(rawUrl);
+ if (!normalized) {
+ return rawUrl.trim();
+ }
+
+ try {
+ const url = new URL(normalized);
+ if (!url.pathname.endsWith('/')) {
+ url.pathname = `${url.pathname}/`;
+ }
+ return url.toString();
+ } catch {
+ return normalized;
+ }
+};
+
+const getLocalOrigin = (): string => {
+ if (typeof window === 'undefined') return '';
+ return window.__OPENCHAMBER_LOCAL_ORIGIN__ || window.location.origin;
+};
+
+const makeId = (): string => {
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
+ return crypto.randomUUID();
+ }
+ return `host-${Date.now()}-${Math.random().toString(16).slice(2)}`;
+};
+
+const statusDotClass = (status: HostProbeResult['status'] | null): string => {
+ if (status === 'ok') return 'bg-status-success';
+ if (status === 'auth') return 'bg-status-warning';
+ if (status === 'unreachable') return 'bg-status-error';
+ return 'bg-muted-foreground/40';
+};
+
+const statusLabel = (status: HostProbeResult['status'] | null): string => {
+ if (status === 'ok') return 'Connected';
+ if (status === 'auth') return 'Auth required';
+ if (status === 'unreachable') return 'Unreachable';
+ return 'Unknown';
+};
+
+const statusIcon = (status: HostProbeResult['status'] | null) => {
+ if (status === 'ok') return ;
+ if (status === 'auth') return ;
+ if (status === 'unreachable') return ;
+ return ;
+};
+
+const sleep = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
+
+const sshPhaseLabel = (phase: DesktopSshInstanceStatus['phase'] | undefined): string => {
+ switch (phase) {
+ case 'ready':
+ return 'Ready';
+ case 'error':
+ return 'Error';
+ case 'degraded':
+ return 'Reconnecting';
+ case 'config_resolved':
+ return 'Resolving config';
+ case 'auth_check':
+ return 'Checking auth';
+ case 'master_connecting':
+ return 'Connecting SSH';
+ case 'remote_probe':
+ return 'Probing remote';
+ case 'installing':
+ return 'Installing';
+ case 'updating':
+ return 'Updating';
+ case 'server_detecting':
+ return 'Detecting server';
+ case 'server_starting':
+ return 'Starting server';
+ case 'forwarding':
+ return 'Forwarding ports';
+ default:
+ return 'Idle';
+ }
+};
+
+const sshPhaseToHostStatus = (
+ phase: DesktopSshInstanceStatus['phase'] | undefined,
+): HostProbeResult['status'] | null => {
+ if (!phase || phase === 'idle') return null;
+ if (phase === 'ready') return 'ok';
+ if (phase === 'error') return 'unreachable';
+ return 'auth';
+};
+
+const getSshStatusById = async (): Promise> => {
+ const statuses = await desktopSshStatus().catch(() => []);
+ const next: Record = {};
+ for (const status of statuses) {
+ next[status.id] = status;
+ }
+ return next;
+};
+
+const waitForSshReady = async (
+ id: string,
+ timeoutMs: number,
+ onUpdate: (status: DesktopSshInstanceStatus) => void,
+ shouldCancel?: () => boolean,
+): Promise => {
+ const deadline = Date.now() + timeoutMs;
+ while (Date.now() < deadline) {
+ if (shouldCancel?.()) {
+ throw new Error(SSH_CONNECT_CANCELLED_ERROR);
+ }
+
+ const statuses = await desktopSshStatus(id).catch(() => []);
+ const status = statuses.find((item) => item.id === id);
+ if (status) {
+ onUpdate(status);
+ if (status.phase === 'ready') {
+ return status;
+ }
+ if (status.phase === 'error') {
+ throw new Error(status.detail || 'SSH connection failed');
+ }
+ }
+ await sleep(700);
+ }
+
+ if (shouldCancel?.()) {
+ throw new Error(SSH_CONNECT_CANCELLED_ERROR);
+ }
+
+ throw new Error('Timed out waiting for SSH connection');
+};
+
+const buildLocalHost = (): DesktopHost => ({
+ id: LOCAL_HOST_ID,
+ label: 'Local',
+ url: getLocalOrigin(),
+});
+
+const resolveCurrentHost = (hosts: DesktopHost[]) => {
+ const currentHref = typeof window === 'undefined' ? '' : window.location.href;
+ const localOrigin = getLocalOrigin();
+ const normalizedLocal = normalizeHostUrl(localOrigin) || localOrigin;
+ const normalizedCurrent = normalizeHostUrl(currentHref) || currentHref;
+
+ if (currentHref && locationMatchesHost(currentHref, localOrigin)) {
+ return { id: LOCAL_HOST_ID, label: 'Local', url: normalizedLocal };
+ }
+
+ const match = hosts.find((h) => {
+ return currentHref ? locationMatchesHost(currentHref, h.url) : false;
+ });
+
+ if (match) {
+ return { id: match.id, label: match.label, url: normalizeHostUrl(match.url) || match.url };
+ }
+
+ return {
+ id: 'custom',
+ label: redactSensitiveUrl(normalizedCurrent || 'Instance'),
+ url: normalizedCurrent,
+ };
+};
+
+type DesktopHostSwitcherDialogProps = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ embedded?: boolean;
+ onHostSwitched?: () => void;
+};
+
+export function DesktopHostSwitcherDialog({
+ open,
+ onOpenChange,
+ embedded = false,
+ onHostSwitched,
+}: DesktopHostSwitcherDialogProps) {
+ const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
+ const setSettingsPage = useUIStore((state) => state.setSettingsPage);
+
+ const [configHosts, setConfigHosts] = React.useState([]);
+ const [defaultHostId, setDefaultHostId] = React.useState(null);
+ const [statusById, setStatusById] = React.useState>({});
+ const [isLoading, setIsLoading] = React.useState(false);
+ const [isProbing, setIsProbing] = React.useState(false);
+ const [isSaving, setIsSaving] = React.useState(false);
+ const [switchingHostId, setSwitchingHostId] = React.useState(null);
+ const [sshHostIds, setSshHostIds] = React.useState>({});
+ const [sshStatusesById, setSshStatusesById] = React.useState>({});
+ const [sshSwitchModal, setSshSwitchModal] = React.useState<{
+ open: boolean;
+ hostId: string | null;
+ hostLabel: string;
+ phase: DesktopSshInstanceStatus['phase'] | 'idle';
+ detail: string | null;
+ error: string | null;
+ }>({
+ open: false,
+ hostId: null,
+ hostLabel: '',
+ phase: 'idle',
+ detail: null,
+ error: null,
+ });
+ const [error, setError] = React.useState('');
+
+ const [editingId, setEditingId] = React.useState(null);
+ const [editLabel, setEditLabel] = React.useState('');
+ const [editUrl, setEditUrl] = React.useState('');
+
+ const [newLabel, setNewLabel] = React.useState('');
+ const [newUrl, setNewUrl] = React.useState('');
+ const [isAddFormOpen, setIsAddFormOpen] = React.useState(!embedded);
+ const sshSwitchTokenRef = React.useRef(0);
+
+ const allHosts = React.useMemo(() => {
+ const local = buildLocalHost();
+ const normalizedRemote = configHosts.map((h) => ({
+ ...h,
+ url: normalizeHostUrl(h.url) || h.url,
+ }));
+ return [local, ...normalizedRemote];
+ }, [configHosts]);
+
+ const current = React.useMemo(() => resolveCurrentHost(allHosts), [allHosts]);
+ const currentDefaultLabel = React.useMemo(() => {
+ const id = defaultHostId || LOCAL_HOST_ID;
+ return allHosts.find((h) => h.id === id)?.label || 'Local';
+ }, [allHosts, defaultHostId]);
+
+ const persist = React.useCallback(async (nextHosts: DesktopHost[], nextDefaultHostId: string | null) => {
+ if (!isTauriShell()) return;
+ setIsSaving(true);
+ setError('');
+ try {
+ const remote = nextHosts.filter((h) => h.id !== LOCAL_HOST_ID);
+ await desktopHostsSet({ hosts: remote, defaultHostId: nextDefaultHostId });
+ setConfigHosts(remote);
+ setDefaultHostId(nextDefaultHostId);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to save');
+ } finally {
+ setIsSaving(false);
+ }
+ }, []);
+
+ const openRemoteInstancesSettings = React.useCallback(() => {
+ setSettingsPage('remote-instances');
+ setSettingsDialogOpen(true);
+ onOpenChange(false);
+ }, [onOpenChange, setSettingsDialogOpen, setSettingsPage]);
+
+ const refresh = React.useCallback(async () => {
+ if (!isTauriShell()) return;
+ setIsLoading(true);
+ setError('');
+ try {
+ const [cfg, sshCfg, sshStatusMap] = await Promise.all([
+ desktopHostsGet(),
+ desktopSshInstancesGet().catch(() => ({ instances: [] })),
+ getSshStatusById(),
+ ]);
+ const nextSshHostIds: Record = {};
+ for (const instance of sshCfg.instances) {
+ nextSshHostIds[instance.id] = true;
+ }
+ setConfigHosts(cfg.hosts || []);
+ setDefaultHostId(cfg.defaultHostId ?? null);
+ setSshHostIds(nextSshHostIds);
+ setSshStatusesById(sshStatusMap);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to load');
+ setConfigHosts([]);
+ setDefaultHostId(null);
+ setSshHostIds({});
+ setSshStatusesById({});
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ const probeAll = React.useCallback(async (hosts: DesktopHost[]) => {
+ if (!isTauriShell()) return;
+ setIsProbing(true);
+ try {
+ const results = await Promise.all(
+ hosts.map(async (h) => {
+ const url = normalizeHostUrl(h.url);
+ if (!url) {
+ return [h.id, { status: 'unreachable' as const, latencyMs: 0 } satisfies HostStatus] as const;
+ }
+ const res = await desktopHostProbe(url).catch((): HostProbeResult => ({ status: 'unreachable', latencyMs: 0 }));
+ return [h.id, { status: res.status, latencyMs: res.latencyMs } satisfies HostStatus] as const;
+ })
+ );
+ const next: Record = {};
+ for (const [id, val] of results) {
+ next[id] = val;
+ }
+ setStatusById(next);
+ } finally {
+ setIsProbing(false);
+ }
+ }, []);
+
+ React.useEffect(() => {
+ if (!open) {
+ setEditingId(null);
+ setEditLabel('');
+ setEditUrl('');
+ setNewLabel('');
+ setNewUrl('');
+ setIsAddFormOpen(!embedded);
+ setSwitchingHostId(null);
+ setSshSwitchModal({ open: false, hostId: null, hostLabel: '', phase: 'idle', detail: null, error: null });
+ setError('');
+ return;
+ }
+ void refresh();
+ }, [embedded, open, refresh]);
+
+ React.useEffect(() => {
+ if (!open) return;
+ void probeAll(allHosts);
+ }, [open, allHosts, probeAll]);
+
+ React.useEffect(() => {
+ if (!open || !isTauriShell()) {
+ return;
+ }
+ let cancelled = false;
+ const run = async () => {
+ const statuses = await getSshStatusById();
+ if (!cancelled) {
+ setSshStatusesById(statuses);
+ }
+ };
+ void run();
+ const interval = window.setInterval(() => {
+ void run();
+ }, 1_500);
+
+ return () => {
+ cancelled = true;
+ window.clearInterval(interval);
+ };
+ }, [open]);
+
+ const handleSwitch = React.useCallback(async (host: DesktopHost) => {
+ const origin = host.id === LOCAL_HOST_ID ? getLocalOrigin() : (normalizeHostUrl(host.url) || '');
+ if (!origin) return;
+
+ const isSshHost = Boolean(sshHostIds[host.id]);
+
+ if (host.id !== LOCAL_HOST_ID && isSshHost && isTauriShell()) {
+ let existingStatus = sshStatusesById[host.id];
+ const latestStatus = await desktopSshStatus(host.id)
+ .then((items) => items.find((item) => item.id === host.id) || null)
+ .catch(() => null);
+ if (latestStatus) {
+ existingStatus = latestStatus;
+ setSshStatusesById((prev) => ({
+ ...prev,
+ [host.id]: latestStatus,
+ }));
+ }
+
+ const existingUrl = normalizeHostUrl(existingStatus?.localUrl || host.url || '');
+ if (existingStatus?.phase === 'ready' && existingUrl) {
+ const target = toNavigationUrl(existingUrl);
+ onHostSwitched?.();
+ window.location.assign(target);
+ return;
+ }
+
+ setSwitchingHostId(host.id);
+ const switchToken = sshSwitchTokenRef.current + 1;
+ sshSwitchTokenRef.current = switchToken;
+ setSshSwitchModal({
+ open: true,
+ hostId: host.id,
+ hostLabel: redactSensitiveUrl(host.label),
+ phase: 'master_connecting',
+ detail: null,
+ error: null,
+ });
+ try {
+ await desktopSshConnect(host.id);
+ if (switchToken !== sshSwitchTokenRef.current) {
+ return;
+ }
+
+ const readyStatus = await waitForSshReady(host.id, SSH_CONNECT_TIMEOUT_MS, (status) => {
+ setSshStatusesById((prev) => ({
+ ...prev,
+ [status.id]: status,
+ }));
+ setSshSwitchModal((prev) => ({
+ ...prev,
+ phase: status.phase,
+ detail: status.detail || null,
+ }));
+ }, () => switchToken !== sshSwitchTokenRef.current);
+
+ if (switchToken !== sshSwitchTokenRef.current) {
+ return;
+ }
+
+ const targetOrigin = normalizeHostUrl(readyStatus.localUrl || '') || origin;
+ const target = toNavigationUrl(targetOrigin);
+ onHostSwitched?.();
+ window.location.assign(target);
+ return;
+ } catch (err) {
+ if (switchToken !== sshSwitchTokenRef.current) {
+ return;
+ }
+
+ const message = err instanceof Error ? err.message : String(err);
+ if (message === SSH_CONNECT_CANCELLED_ERROR) {
+ return;
+ }
+
+ setSshSwitchModal((prev) => ({
+ ...prev,
+ error: message,
+ }));
+ toast.error(`SSH instance "${redactSensitiveUrl(host.label)}" failed to connect`, {
+ description: message,
+ });
+ return;
+ } finally {
+ if (switchToken === sshSwitchTokenRef.current) {
+ setSwitchingHostId(null);
+ }
+ }
+ }
+
+ if (host.id !== LOCAL_HOST_ID && isTauriShell()) {
+ setSwitchingHostId(host.id);
+ const probe = await desktopHostProbe(origin).catch((): HostProbeResult => ({ status: 'unreachable', latencyMs: 0 }));
+ setStatusById((prev) => ({
+ ...prev,
+ [host.id]: { status: probe.status, latencyMs: probe.latencyMs },
+ }));
+
+ if (probe.status === 'unreachable') {
+ toast.error(`Instance "${redactSensitiveUrl(host.label)}" is unreachable`);
+ setSwitchingHostId(null);
+ return;
+ }
+ }
+
+ const target = toNavigationUrl(origin);
+ onHostSwitched?.();
+
+ try {
+ window.location.assign(target);
+ } catch {
+ window.location.href = target;
+ }
+ }, [onHostSwitched, sshHostIds, sshStatusesById]);
+
+ const beginEdit = React.useCallback((host: DesktopHost) => {
+ setEditingId(host.id);
+ setEditLabel(host.label);
+ setEditUrl(host.url);
+ setError('');
+ }, []);
+
+ const cancelEdit = React.useCallback(() => {
+ setEditingId(null);
+ setEditLabel('');
+ setEditUrl('');
+ }, []);
+
+ const commitEdit = React.useCallback(async () => {
+ if (!editingId) return;
+ if (editingId === LOCAL_HOST_ID) {
+ cancelEdit();
+ return;
+ }
+
+ const url = normalizeHostUrl(editUrl);
+ if (!url) {
+ setError('Invalid URL (must be http/https)');
+ return;
+ }
+
+ const label = (editLabel || redactSensitiveUrl(url)).trim();
+ const nextHosts = configHosts.map((h) => (h.id === editingId ? { ...h, label, url } : h));
+ await persist(nextHosts, defaultHostId);
+ cancelEdit();
+ }, [cancelEdit, configHosts, defaultHostId, editLabel, editUrl, editingId, persist]);
+
+ const addHost = React.useCallback(async () => {
+ const url = normalizeHostUrl(newUrl);
+ if (!url) {
+ setError('Invalid URL (must be http/https)');
+ return;
+ }
+ const label = (newLabel || redactSensitiveUrl(url)).trim();
+ const id = makeId();
+
+ const nextHosts = [{ id, label, url }, ...configHosts];
+ await persist(nextHosts, defaultHostId);
+ setNewLabel('');
+ setNewUrl('');
+ if (embedded) {
+ setIsAddFormOpen(false);
+ }
+ }, [configHosts, defaultHostId, embedded, newLabel, newUrl, persist]);
+
+ const deleteHost = React.useCallback(async (id: string) => {
+ if (id === LOCAL_HOST_ID) return;
+ const nextHosts = configHosts.filter((h) => h.id !== id);
+ const nextDefault = defaultHostId === id ? LOCAL_HOST_ID : defaultHostId;
+ await persist(nextHosts, nextDefault);
+ }, [configHosts, defaultHostId, persist]);
+
+ const setDefault = React.useCallback(async (id: string) => {
+ const next = id === LOCAL_HOST_ID ? LOCAL_HOST_ID : id;
+ await persist(configHosts, next);
+ }, [configHosts, persist]);
+
+ const openInNewWindow = React.useCallback((host: DesktopHost) => {
+ const origin = host.id === LOCAL_HOST_ID ? getLocalOrigin() : (normalizeHostUrl(host.url) || '');
+ if (!origin) return;
+ const target = toNavigationUrl(origin);
+ desktopOpenNewWindowAtUrl(target).catch((err: unknown) => {
+ toast.error('Failed to open new window', {
+ description: err instanceof Error ? err.message : String(err),
+ });
+ });
+ }, []);
+
+ const switchToLocal = React.useCallback(() => {
+ sshSwitchTokenRef.current += 1;
+ setSwitchingHostId(null);
+ setSshSwitchModal((prev) => ({
+ ...prev,
+ open: false,
+ hostId: null,
+ error: null,
+ detail: null,
+ phase: 'idle',
+ }));
+ const localTarget = toNavigationUrl(getLocalOrigin());
+ onHostSwitched?.();
+ window.location.assign(localTarget);
+ }, [onHostSwitched]);
+
+ const cancelSshSwitch = React.useCallback(async () => {
+ const hostId = sshSwitchModal.hostId || switchingHostId;
+ sshSwitchTokenRef.current += 1;
+ setSwitchingHostId(null);
+ setSshSwitchModal({
+ open: false,
+ hostId: null,
+ hostLabel: '',
+ phase: 'idle',
+ detail: null,
+ error: null,
+ });
+
+ if (!hostId || hostId === LOCAL_HOST_ID || !isTauriShell()) {
+ return;
+ }
+
+ await desktopSshDisconnect(hostId).catch(() => {});
+ }, [sshSwitchModal.hostId, switchingHostId]);
+
+ const retrySshSwitch = React.useCallback(() => {
+ const hostId = sshSwitchModal.hostId;
+ if (!hostId) return;
+ const host = allHosts.find((item) => item.id === hostId);
+ if (!host) return;
+ void handleSwitch(host);
+ }, [allHosts, handleSwitch, sshSwitchModal.hostId]);
+
+ const connectSshHostInPlace = React.useCallback(async (host: DesktopHost) => {
+ if (!isTauriShell()) return;
+ setSwitchingHostId(host.id);
+ try {
+ await desktopSshConnect(host.id);
+ const readyStatus = await waitForSshReady(host.id, SSH_CONNECT_TIMEOUT_MS, (status) => {
+ setSshStatusesById((prev) => ({
+ ...prev,
+ [status.id]: status,
+ }));
+ });
+ if (readyStatus.phase === 'ready') {
+ toast.success(`SSH instance "${redactSensitiveUrl(host.label)}" connected`);
+ }
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ if (message !== SSH_CONNECT_CANCELLED_ERROR) {
+ toast.error(`SSH instance "${redactSensitiveUrl(host.label)}" failed to connect`, {
+ description: message,
+ });
+ }
+ } finally {
+ setSwitchingHostId(null);
+ }
+ }, []);
+
+ if (!isDesktopShell()) {
+ return null;
+ }
+
+ const tauriAvailable = isTauriShell();
+
+ const content = (
+ <>
+ {embedded ? (
+
+
+
+ Current
+ {redactSensitiveUrl(current.label)}
+ •
+ Default
+ {redactSensitiveUrl(currentDefaultLabel)}
+
+
void probeAll(allHosts)}
+ disabled={!tauriAvailable || isLoading || isProbing}
+ aria-label="Refresh instances"
+ >
+
+
+
+
+ ) : (
+
+
+
+ Instance
+
+
+ Switch between Local and remote OpenChamber servers
+
+
+ )}
+
+ {!embedded && (
+
+
+ Current:
+ {redactSensitiveUrl(current.label)}
+ Current default:
+ {redactSensitiveUrl(currentDefaultLabel)}
+
+
+ void probeAll(allHosts)}
+ disabled={!tauriAvailable || isLoading || isProbing}
+ >
+
+ Refresh
+
+
+
+ )}
+
+ {tauriAvailable && (
+
+
+ Need SSH instances? Manage them in Settings.
+
+
+ Remote SSH
+
+
+
+ )}
+
+ {!tauriAvailable && (
+
+
+ Instance switcher is limited on this page. Use Local to recover.
+
+
+ )}
+
+
+
+ {isLoading ? (
+
Loading…
+ ) : (
+ allHosts.map((host) => {
+ const isLocal = host.id === LOCAL_HOST_ID;
+ const isSsh = Boolean(sshHostIds[host.id]);
+ const isActive = host.id === current.id;
+ const isDefault = (defaultHostId || LOCAL_HOST_ID) === host.id;
+ const status = statusById[host.id] || null;
+ const sshStatus = sshStatusesById[host.id] || null;
+ const statusKind = isSsh ? sshPhaseToHostStatus(sshStatus?.phase) : (status?.status ?? null);
+ const isEditing = editingId === host.id;
+ const effectiveUrl = isLocal ? getLocalOrigin() : (normalizeHostUrl(host.url) || host.url);
+ const displayLabel = redactSensitiveUrl(host.label);
+ const displayUrl = redactSensitiveUrl(effectiveUrl);
+
+ return (
+
+
void handleSwitch(host)}
+ disabled={switchingHostId === host.id}
+ aria-label={`Switch to ${displayLabel}`}
+ >
+
+
+
+
+ {displayLabel}
+
+ {isSsh && (
+
+ SSH
+
+ )}
+ {isActive && (
+ Current
+ )}
+
+ {statusIcon(statusKind)}
+
+ {isSsh ? sshPhaseLabel(sshStatus?.phase) : statusLabel(status?.status ?? null)}
+ {!isSsh && status?.status === 'ok' && typeof status.latencyMs === 'number' ? ` · ${Math.max(0, Math.round(status.latencyMs))}ms ping` : ''}
+
+
+
+
+ {displayUrl}
+
+
+
+
+
+ {!isLocal && !isSsh && (
+
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ {
+ e.stopPropagation();
+ beginEdit(host);
+ }}
+ disabled={isSaving}
+ >
+
+ Edit
+
+ {
+ e.stopPropagation();
+ void deleteHost(host.id);
+ }}
+ className="text-destructive focus:text-destructive"
+ disabled={isSaving}
+ >
+
+ Delete
+
+
+
+ )}
+
+ {isLocal && (
+
+ )}
+
+ {isSsh && !isLocal && (
+ (sshStatus?.phase === 'idle' || !sshStatus?.phase) ? (
+
{
+ e.stopPropagation();
+ void connectSshHostInPlace(host);
+ }}
+ >
+ {switchingHostId === host.id ? : }
+ Connect
+
+ ) : (
+
+ )
+ )}
+
+
+
+ void setDefault(host.id)}
+ aria-label={isDefault ? 'Default instance' : 'Set as default'}
+ disabled={isSaving}
+ >
+ {isDefault ? : }
+
+
+
+ {isDefault ? 'Default' : 'Set as default'}
+
+
+
+
+
+ {
+ e.stopPropagation();
+ openInNewWindow(host);
+ }}
+ disabled={statusKind === 'unreachable'}
+ aria-label="Open in new window"
+ >
+
+
+
+
+ {statusKind === 'unreachable' ? 'Instance unreachable' : 'Open in new window'}
+
+
+
+
+ );
+ })
+ )}
+
+
+
+ {tauriAvailable && editingId && editingId !== LOCAL_HOST_ID && (
+
+
+
Edit instance
+
+
+ Cancel
+
+ void commitEdit()} disabled={isSaving}>
+ {isSaving ? : null}
+ Save
+
+
+
+
+ setEditLabel(e.target.value)}
+ placeholder="Label"
+ disabled={isSaving}
+ />
+ setEditUrl(e.target.value)}
+ placeholder="https://host:port"
+ disabled={isSaving}
+ />
+
+
+ )}
+
+ {embedded && !isAddFormOpen ? (
+
+ setIsAddFormOpen(true)}
+ disabled={!tauriAvailable || isSaving}
+ >
+
+ Add instance
+
+
+ ) : (
+
+
+
Add instance
+
+ {embedded && (
+ setIsAddFormOpen(false)}
+ disabled={isSaving}
+ >
+ Cancel
+
+ )}
+ void addHost()}
+ disabled={!tauriAvailable || isSaving || !newUrl.trim()}
+ >
+ {isSaving ? : null}
+ Add
+
+
+
+
+ setNewLabel(e.target.value)}
+ placeholder="Label (optional)"
+ disabled={!tauriAvailable || isSaving}
+ />
+ setNewUrl(e.target.value)}
+ placeholder="https://host:port"
+ disabled={!tauriAvailable || isSaving}
+ />
+
+
+ )}
+
+ {error && (
+ {error}
+ )}
+ >
+ );
+
+ const sshSwitchDialog = (
+ {
+ if (!nextOpen && switchingHostId) {
+ void cancelSshSwitch();
+ return;
+ }
+ setSshSwitchModal((prev) => ({
+ ...prev,
+ open: nextOpen,
+ ...(nextOpen ? {} : { hostId: null, error: null, detail: null, phase: 'idle' as const }),
+ }));
+ }}
+ >
+
+
+
+
+ Connecting to {sshSwitchModal.hostLabel || 'SSH instance'}
+
+
+ {sshSwitchModal.error
+ ? sshSwitchModal.error
+ : sshSwitchModal.detail || sshPhaseLabel(sshSwitchModal.phase)}
+
+
+ {sshSwitchModal.error ? (
+
+
+ Switch to Local
+
+
+ Retry
+
+
+ ) : null}
+
+
+ );
+
+ if (embedded) {
+ return (
+ <>
+
+ {content}
+
+ {sshSwitchDialog}
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+ {content}
+
+
+ {sshSwitchDialog}
+ >
+ );
+}
+
+type DesktopHostSwitcherButtonProps = {
+ headerIconButtonClass: string;
+};
+
+export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHostSwitcherButtonProps) {
+ const [open, setOpen] = React.useState(false);
+ const [label, setLabel] = React.useState('Local');
+ const [status, setStatus] = React.useState(null);
+ const attemptedDefaultSshConnectRef = React.useRef(false);
+ const [startupSshModal, setStartupSshModal] = React.useState<{
+ open: boolean;
+ hostId: string | null;
+ hostLabel: string;
+ error: string | null;
+ connecting: boolean;
+ }>({
+ open: false,
+ hostId: null,
+ hostLabel: '',
+ error: null,
+ connecting: false,
+ });
+
+ const connectDefaultSshInstance = React.useCallback(async (
+ hostId: string,
+ hostLabel: string,
+ options?: { showProgress?: boolean },
+ ): Promise => {
+ const showProgress = Boolean(options?.showProgress);
+ if (showProgress) {
+ setStartupSshModal({
+ open: true,
+ hostId,
+ hostLabel,
+ error: null,
+ connecting: true,
+ });
+ }
+
+ try {
+ await desktopSshConnect(hostId);
+ const ready = await waitForSshReady(hostId, 45_000, () => {});
+ const localUrl = normalizeHostUrl(ready.localUrl || '');
+ if (!localUrl) {
+ throw new Error('Connected but missing forwarded URL');
+ }
+ window.location.assign(toNavigationUrl(localUrl));
+ return true;
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ setStartupSshModal({
+ open: true,
+ hostId,
+ hostLabel,
+ error: message,
+ connecting: false,
+ });
+ return false;
+ }
+ }, []);
+
+ const switchStartupToLocal = React.useCallback(async () => {
+ setStartupSshModal({
+ open: false,
+ hostId: null,
+ hostLabel: '',
+ error: null,
+ connecting: false,
+ });
+
+ await desktopHostsGet()
+ .then((cfg) => desktopHostsSet({ hosts: cfg.hosts, defaultHostId: LOCAL_HOST_ID }))
+ .catch(() => undefined);
+
+ window.location.assign(toNavigationUrl(getLocalOrigin()));
+ }, []);
+
+ const retryStartupSsh = React.useCallback(() => {
+ const hostId = startupSshModal.hostId;
+ if (!hostId) return;
+ void connectDefaultSshInstance(hostId, startupSshModal.hostLabel || 'SSH instance', {
+ showProgress: true,
+ });
+ }, [connectDefaultSshInstance, startupSshModal.hostId, startupSshModal.hostLabel]);
+
+ React.useEffect(() => {
+ if (!isTauriShell()) return;
+
+ let cancelled = false;
+ const run = async () => {
+ try {
+ const cfg = await desktopHostsGet();
+ const local = buildLocalHost();
+ const all = [local, ...(cfg.hosts || [])];
+ const current = resolveCurrentHost(all);
+
+ if (
+ !attemptedDefaultSshConnectRef.current &&
+ current.id === LOCAL_HOST_ID &&
+ cfg.defaultHostId &&
+ cfg.defaultHostId !== LOCAL_HOST_ID
+ ) {
+ const sshCfg = await desktopSshInstancesGet().catch(() => ({ instances: [] }));
+ const defaultSsh = sshCfg.instances.find((instance) => instance.id === cfg.defaultHostId);
+ if (defaultSsh) {
+ attemptedDefaultSshConnectRef.current = true;
+ const hostLabel = redactSensitiveUrl(
+ defaultSsh.nickname?.trim() || defaultSsh.sshParsed?.destination || defaultSsh.id,
+ );
+ const connected = await connectDefaultSshInstance(cfg.defaultHostId, hostLabel);
+ if (connected || cancelled) {
+ return;
+ }
+ }
+ }
+
+ if (cancelled) return;
+ setLabel(redactSensitiveUrl(current.label || 'Instance'));
+ const normalized = normalizeHostUrl(current.url);
+ if (!normalized) {
+ setStatus(null);
+ return;
+ }
+ const res = await desktopHostProbe(normalized).catch((): HostProbeResult => ({ status: 'unreachable', latencyMs: 0 }));
+ if (cancelled) return;
+ setStatus(res.status);
+ } catch {
+ if (!cancelled) {
+ setLabel('Instance');
+ setStatus(null);
+ }
+ }
+ };
+
+ void run();
+ const interval = window.setInterval(() => {
+ void run();
+ }, 10_000);
+ return () => {
+ cancelled = true;
+ window.clearInterval(interval);
+ };
+ }, [connectDefaultSshInstance]);
+
+ if (!isDesktopShell()) {
+ return null;
+ }
+
+ const isCurrentlyLocal = locationMatchesHost(window.location.href, getLocalOrigin());
+
+ const fallbackLabel = typeof window !== 'undefined' && window.location.hostname
+ ? window.location.hostname
+ : 'Instance';
+
+ const effectiveLabel = isCurrentlyLocal
+ ? 'Local'
+ : label === 'Local'
+ ? fallbackLabel
+ : label;
+ const safeEffectiveLabel = redactSensitiveUrl(effectiveLabel);
+
+ return (
+ <>
+
+
+ setOpen(true)}
+ aria-label="Switch instance"
+ data-oc-host-switcher
+ className={cn(headerIconButtonClass, 'relative w-auto px-3')}
+ >
+
+
+ {safeEffectiveLabel}
+
+
+
+
+
+ Instance
+
+
+
+ {
+ if (!nextOpen && startupSshModal.connecting) {
+ return;
+ }
+ if (!nextOpen) {
+ setStartupSshModal((prev) => ({
+ ...prev,
+ open: false,
+ connecting: false,
+ }));
+ return;
+ }
+ setStartupSshModal((prev) => ({ ...prev, open: true }));
+ }}
+ >
+
+
+ Default SSH instance unavailable
+
+ {startupSshModal.connecting
+ ? `Connecting to ${startupSshModal.hostLabel || 'SSH instance'}...`
+ : startupSshModal.error || 'Failed to connect the default SSH instance.'}
+
+
+
+ void switchStartupToLocal()}
+ disabled={startupSshModal.connecting}
+ >
+ Switch to Local
+
+
+ {startupSshModal.connecting ? : null}
+ Retry
+
+
+
+
+ >
+ );
+}
+
+export function DesktopHostSwitcherInline() {
+ const [open, setOpen] = React.useState(false);
+
+ if (!isDesktopShell()) {
+ return null;
+ }
+
+ return (
+ <>
+ setOpen(true)}
+ >
+
+ Switch instance
+
+
+ >
+ );
+}
diff --git a/ui/src/components/desktop/OpenInAppButton.tsx b/ui/src/components/desktop/OpenInAppButton.tsx
new file mode 100644
index 0000000..82a0def
--- /dev/null
+++ b/ui/src/components/desktop/OpenInAppButton.tsx
@@ -0,0 +1,214 @@
+import React from 'react';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { toast } from '@/components/ui';
+import { copyTextToClipboard } from '@/lib/clipboard';
+import { cn } from '@/lib/utils';
+import { isDesktopLocalOriginActive, isTauriShell, openDesktopPath, openDesktopProjectInApp } from '@/lib/desktop';
+import { DEFAULT_OPEN_IN_APP_ID, OPEN_IN_APPS } from '@/lib/openInApps';
+import { useOpenInAppsStore, type OpenInAppOption } from '@/stores/useOpenInAppsStore';
+import { RiArrowDownSLine, RiCheckLine, RiFileCopyLine, RiRefreshLine } from '@remixicon/react';
+
+const FINDER_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAJAAAAABAAAAkAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAAB+C9pSAAAACXBIWXMAABYlAAAWJQFJUiTwAAABnWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yNTY8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MjU2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl6wHhsAAAXaSURBVFgJ7VddbBRVFP5mdme6dOnu2tZawOAPjfxU+bGQYCRAsvw8qNGEQPTRJ0I0amL0wfjgA/HBR8KLD8YgDxJEUkVFxSYYTSRii6DQQCMGIQqlW7p0u+zMzo/fuTuzO9Nt0Td94CRn7pl7z5zznZ977y5wh/7jDGiz+T979qD5Ujbfd90xlll+stOF1uI40B1+4HhkjnZk9CgLQ9iXp2/BdcbgVc/h0sAgduywudJEMwLY9Of4ugtW5p3CpL7W1jTN88VmjdQYvnDKF1mczkYuNZLeCVg3X8fa9u+nqzUB2HRpdN2pSseRQknPoUL1Jo2ICTrPGcCzdwPdHENcAnicKRqcAk7cpL5J1r0JlAtPYV1XDETM/FtH3m19r+f5by+XjNX/xnmCX3/cCzydi4CKiC7lw+PArhGgoPPFq/6E0+9vwM6d5VBNpuv03cLNfeNTRh9KnJIiV2/PvSngycC5RD+dE5zb3g7s6QESzAZc2l6wuY9SnWIAxv10r81uU85Vt1FvtpEtlc/SMFUkUofeZ2IBta0DWDmXgkfbyTRz1qAYAMczOz3p1elOxYPyEllj421hdELViPO6Kudk3ia3UGe5ABDbvtnJZ52SdYmCZ3stdeexBabFdeAbYopEowtagVUZqFapBrtAGqpiVaFrGgyjZlrmTD5yEqoEJj4iFMuA62i6L3WPZkAiuHgarZ/vbWSBkTzO2rfTR4XOJVJhjfX44MBn+OTocVWbcF5MalxXPeVL6zYonoGo44YOtDI7qHC1lkL5nHnOc+tJRi3K6iygLNGMjt1A1XVV6iUzOvVtAvMlS2I/yBYlRf8MgA6szmXQ1jDfKhSgjft6DRtrkgarAiAw5nI9v2WDSn+Zxfd9DawGxIlPPQUg0A2HGABfEIYlCDU4+q0d8O+jRzHCCFYy+nu4BaeYAoksBCDrPYsXQQ6iitgiSQaS1FHHtMzFil4DpxTl4UhORSn4WOaaiGsbu4iFRkMnYQlEV0oSJQGQ4FyYgSRDjpqPZcCR6EOOWonIEsBqArAIQOMLzw0VXRRERF2VoA6Atk1+MzsASekMJYgaFEeHR4Cr85lNGntYzgKCYd/NSNIDCXr0ZJ2jwTsjSvEMzFQCCVmKHBRahn2DNb4rDRx8pnbXOOIg0JELLMHOF1AUkaRj1V8c2TookkMS83WK9QCVpRwtf5wCykQWRKDyJ44Ytc452QUV6inmN9IDIv/6y2+YLDuqTywBEHxv8rsoxQC4Fpf4cZ2pbJ4/huxXr0EvFmoRCrAIVymLQ3Eid0GJYPsPfISBLwdwi79YQnCqBNS7LQDP5qYSAKEDypOrX4WVWYLsFy+i9cwh6CUmUKIJI2Gq5cSbnLLw849D2Ld3L4olC1u3P0c1ow5Ozgixa3puWChONG1D3eLZUQOglvng+Vp5dBfseesx5/yHyI4cBTL3wsssRGs2g6/ppHijiMLoNSSMNHofy6Nn6SPsAR02nUoTtrDTSrdoi8CTni55rlOsCf1ypaDxlFMNU1epCV5XL6Y6dmOq+BeS48NIlq7Anpjg5dOFbPdDWLQyj/aubnUKSkMKi3NhkUd4kieYtbRbYS0bFAOQKI8NO363z1RJHmamtnlwhGksxV2w/gl29WRtm8kWtWUnRShLnQvXgDOXmLg2HzlvbDiyHD8Y517YP2i4FtueFPbB9FFqKcyobk4A5y7zquUFa7IXojyHoeXmAFcY755vaI6A56Xsofm/7+cmblBTpOldQ5vs3PJDVS+RVSAaus2SpJTO80t4NTNSOQfCDrtFkBevA0ME6HGvPdDpFlekzm7rf3nFQNRQEwBZTL9warObWfx21Uv1+fx1ERqVNampGoOHpF1tsdp07RnoGMxK1vT97rbK4IP6+Tc+fWXVsahaYGL6VO09d//GXHXr7jVeqmuppqU6ff4x0RO6lqRxgxHJpWKSlcw5eWfjq5rq/CdhaL5l6JWxjDc6bP7w5sn+/uMs2B36H2bgb6v9raK0+o9IAAAAAElFTkSuQmCC';
+const TERMINAL_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAJAAAAABAAAAkAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAAB+C9pSAAAACXBIWXMAABYlAAAWJQFJUiTwAAABnWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yNTY8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MjU2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl6wHhsAAAQzSURBVFgJ7VZNbBNHFH67Xv9RxwnBDqlUoQglcZK6qSIEJIQWAYJQoVY9IE5RTzn20FMvqdpDesq9B24+NdwthAJCkZChJg1JSOXYQIwQKQIaBdtENbs73t2+N8miGWOcpFHUHniyd97OvJ9v3nv7ZgDe038cAeVd/jOZjC94sKdfU+Bj24G9igpexwYPyiu2bauKqqqirkOTqmrjnIOyFsoyUKDocSCj/7mU7ujoMER5l68JYOFZ4YSiwPjd9O0jjx7ch1KhAJZVAcdx0LxDv3XetYKjggr4I4bzHo8G4aYmONjZBYf6+2dUzfd9PNowJajUZmef/PX5zcWl0rmvvnbQHrra+f/M+S+dqYXs2t3Hz09Ve5UicCmZ3NPb1Zv66btv+65dSULA64WGxkbw+Xx8V9XK9d4pWowxeFUqgW6acHroC/j5l0sLD/PZY98MDf3t6mouQ+On3X1H7/2e7rtOztHpgbY2+CAUgperq+D3+7cNgtLSEA7D0+VluDF5FS7cSff2HT56DF1dd/3KhQTWJ/lclsc8jIrk9IfRURgZGQEvRqNSWa8D2t1W/liXXK8Ro0i0lF0ExaPEXec0SgAqhrm3VCzwdS9GQNd1GBsbg0AgAIlEAlpbW7EYLVF/U56AagieiGwbuhERlSQApmEE8c/XKXxU0fF4HNowFfPz81Aul7edBjLGbeHITANsZga4g42HVAM2Y74KM/kSIQ/izgcHB2FiYgJmZmZ4MZpYULRG5PF4+Bx/2cLDxuhhYUqFLwGoWCaQEBGhNjAa4+Pj/J3SQA6pHpqbm/kcNitIJpOgaZIZvlbrQbZNJvcjSZOZDKhwRKLic4l2Pjc3B8FgkE+trKxAVUN0RWuOZNtCHyJJACj/bgREIZcnA9PT029SQM63unuywSOwUWOuTQmAhfmnlluPxIjUk6u1RrbJh0jyV0Ap2OZnJhrbjOcRqEqBBMDCAtltAORDJAkAVj2mWS5CUXinPDUx+oxFkgBYjO0qANu2wKoqQgkAfgW7C4AiYMmfoQSgwpjj7GYRUh/Q66SAmdisNxql227FfP1bXrRlVExdtCNHwDRLdPkgwmi8OUREhe3y1NLJFpEfbWMNvBRtSI2o+KqYi+zbx4NQwptMCO8E1HjEHYjKm/HknG5FZIsCG4lEoLS2lhP1JAB3bt1KH//s+GJPd3dPJpvlN5kwXiYIhHukisr1eAItXsm6YzGItrTcn5+dvS3qSQBSqVQhFouNnj039CsaCC7mcqDjgbNT6op1AtrU8Wo3Ojk5KaVAOptdR8PDwxf3t7SMvXjxvJNOPP31a35Krt8CXKl3j2SUDip/IAjRaBRaP9z/cHW18GMikbhcrVUTAAm1t7d/NDAwcDIUCvVqmtqkyLe3ajtvvTtg4x3SLpbLa3+kUr9N5fP55beE3k/8HyLwDx2/HIx7q3WfAAAAAElFTkSuQmCC';
+
+type OpenInAppOptionWithFallback = OpenInAppOption & {
+ fallbackIconDataUrl?: string;
+};
+
+const withFallbackIcon = (app: OpenInAppOption): OpenInAppOptionWithFallback => ({
+ ...app,
+ fallbackIconDataUrl: app.id === 'finder'
+ ? FINDER_DEFAULT_ICON_DATA_URL
+ : app.id === 'terminal'
+ ? TERMINAL_DEFAULT_ICON_DATA_URL
+ : undefined,
+});
+
+const AppIcon = ({
+ label,
+ iconDataUrl,
+ fallbackIconDataUrl,
+}: {
+ label: string;
+ iconDataUrl?: string;
+ fallbackIconDataUrl?: string;
+}) => {
+ const [failed, setFailed] = React.useState(false);
+ const initial = label.trim().slice(0, 1).toUpperCase() || '?';
+
+ const src = iconDataUrl || fallbackIconDataUrl;
+
+ if (src && !failed) {
+ return (
+ setFailed(true)}
+ />
+ );
+ }
+
+ return (
+
+ {initial}
+
+ );
+};
+
+type OpenInAppButtonProps = {
+ directory: string;
+ activeFilePath?: string | null;
+ className?: string;
+};
+
+export const OpenInAppButton = ({ directory, activeFilePath, className }: OpenInAppButtonProps) => {
+ const selectedAppId = useOpenInAppsStore((state) => state.selectedAppId);
+ const availableApps = useOpenInAppsStore((state) => state.availableApps);
+ const isCacheStale = useOpenInAppsStore((state) => state.isCacheStale);
+ const isScanning = useOpenInAppsStore((state) => state.isScanning);
+ const initialize = useOpenInAppsStore((state) => state.initialize);
+ const loadInstalledApps = useOpenInAppsStore((state) => state.loadInstalledApps);
+ const selectApp = useOpenInAppsStore((state) => state.selectApp);
+
+ React.useEffect(() => {
+ initialize();
+ }, [initialize]);
+
+ const isDesktopLocal = isTauriShell() && isDesktopLocalOriginActive();
+
+ const selectedApp = React.useMemo(() => {
+ const known = availableApps.find((app) => app.id === selectedAppId)
+ ?? availableApps.find((app) => app.id === DEFAULT_OPEN_IN_APP_ID)
+ ?? availableApps[0]
+ ?? OPEN_IN_APPS[0];
+ if (known) {
+ return withFallbackIcon(known);
+ }
+ return withFallbackIcon(OPEN_IN_APPS[0]);
+ }, [availableApps, selectedAppId]);
+
+ if (!isDesktopLocal || !directory) {
+ return null;
+ }
+
+ if (availableApps.length === 0) {
+ return null;
+ }
+
+ const handleOpen = async (app: OpenInAppOption) => {
+ const opened = await openDesktopProjectInApp(directory, app.id, app.appName, activeFilePath);
+ if (!opened) {
+ await openDesktopPath(directory, app.appName);
+ }
+ };
+
+ const handleSelect = async (app: OpenInAppOption) => {
+ await selectApp(app.id);
+ await handleOpen(app);
+ };
+
+ const handleCopyPath = async () => {
+ const text = directory;
+ const result = await copyTextToClipboard(text);
+ if (!result.ok) {
+ return;
+ }
+ toast.success('Path copied to clipboard');
+ };
+
+ return (
+
+
void handleOpen(selectedApp)}
+ className={cn(
+ 'inline-flex h-full items-center gap-2 px-3 typography-ui-label font-medium',
+ 'text-foreground hover:bg-interactive-hover transition-colors'
+ )}
+ aria-label={`Open in ${selectedApp.label}`}
+ >
+
+
+ Open
+
+
+
+
+
+
+
+
+
+ void handleCopyPath()}>
+
+ Copy Path
+
+
+ {availableApps.map((app) => {
+ const appWithFallback = withFallbackIcon(app);
+ return (
+ void handleSelect(app)}
+ >
+
+ {app.label}
+ {selectedApp.id === app.id ? (
+
+ ) : null}
+
+ );
+ })}
+ {isCacheStale ? (
+ void loadInstalledApps(true)}
+ >
+
+ Refresh Apps
+
+ ) : null}
+
+
+
+ );
+};
diff --git a/ui/src/components/icons/ArrowsMerge.tsx b/ui/src/components/icons/ArrowsMerge.tsx
new file mode 100644
index 0000000..2bf971c
--- /dev/null
+++ b/ui/src/components/icons/ArrowsMerge.tsx
@@ -0,0 +1,19 @@
+import type { SVGProps } from 'react';
+
+export function ArrowsMerge(props: SVGProps) {
+ return (
+
+
+
+ );
+}
diff --git a/ui/src/components/icons/DiffIcon.tsx b/ui/src/components/icons/DiffIcon.tsx
new file mode 100644
index 0000000..dd46a0b
--- /dev/null
+++ b/ui/src/components/icons/DiffIcon.tsx
@@ -0,0 +1,27 @@
+import type { SVGProps } from 'react';
+
+interface DiffIconProps extends Omit, 'children'> {
+ size?: number | string;
+}
+
+/**
+ * Git merge/branch icon for the Diff tab.
+ */
+export function DiffIcon({ size, className, style, ...props }: DiffIconProps) {
+ return (
+
+
+
+ );
+}
diff --git a/ui/src/components/icons/FileTypeIcon.tsx b/ui/src/components/icons/FileTypeIcon.tsx
new file mode 100644
index 0000000..5acca7a
--- /dev/null
+++ b/ui/src/components/icons/FileTypeIcon.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+import { getFileTypeIconHref } from '@/lib/fileTypeIcons';
+import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
+
+type FileTypeIconProps = {
+ filePath: string;
+ extension?: string;
+ className?: string;
+};
+
+export const FileTypeIcon: React.FC = ({ filePath, extension, className }) => {
+ const theme = useOptionalThemeSystem();
+ const variant = theme?.currentTheme.metadata.variant === 'light' ? 'light' : 'dark';
+ const iconHref = getFileTypeIconHref(filePath, { extension, themeVariant: variant });
+
+ return (
+
+
+
+ );
+};
diff --git a/ui/src/components/icons/McpIcon.tsx b/ui/src/components/icons/McpIcon.tsx
new file mode 100644
index 0000000..b1484cb
--- /dev/null
+++ b/ui/src/components/icons/McpIcon.tsx
@@ -0,0 +1,20 @@
+import type { SVGProps } from 'react';
+
+export function McpIcon(props: SVGProps) {
+ return (
+
+
+
+
+ );
+}
diff --git a/ui/src/components/icons/StopIcon.tsx b/ui/src/components/icons/StopIcon.tsx
new file mode 100644
index 0000000..9b86898
--- /dev/null
+++ b/ui/src/components/icons/StopIcon.tsx
@@ -0,0 +1,20 @@
+import type { SVGProps } from 'react';
+
+export function StopIcon(props: SVGProps) {
+ return (
+
+
+
+
+ );
+}
diff --git a/ui/src/components/layout/BottomTerminalDock.tsx b/ui/src/components/layout/BottomTerminalDock.tsx
new file mode 100644
index 0000000..6751155
--- /dev/null
+++ b/ui/src/components/layout/BottomTerminalDock.tsx
@@ -0,0 +1,193 @@
+import React from 'react';
+import { RiCloseLine, RiFullscreenExitLine, RiFullscreenLine } from '@remixicon/react';
+import { cn } from '@/lib/utils';
+import { useUIStore } from '@/stores/useUIStore';
+
+const BOTTOM_DOCK_MIN_HEIGHT = 180;
+const BOTTOM_DOCK_MAX_HEIGHT = 640;
+const BOTTOM_DOCK_COLLAPSE_THRESHOLD = 110;
+
+interface BottomTerminalDockProps {
+ isOpen: boolean;
+ isMobile: boolean;
+ children: React.ReactNode;
+}
+
+export const BottomTerminalDock: React.FC = ({ isOpen, isMobile, children }) => {
+ const bottomTerminalHeight = useUIStore((state) => state.bottomTerminalHeight);
+ const isFullscreen = useUIStore((state) => state.isBottomTerminalExpanded);
+ const setBottomTerminalHeight = useUIStore((state) => state.setBottomTerminalHeight);
+ const setBottomTerminalOpen = useUIStore((state) => state.setBottomTerminalOpen);
+ const setBottomTerminalExpanded = useUIStore((state) => state.setBottomTerminalExpanded);
+ const [fullscreenHeight, setFullscreenHeight] = React.useState(null);
+ const [isResizing, setIsResizing] = React.useState(false);
+ const dockRef = React.useRef(null);
+ const startYRef = React.useRef(0);
+ const startHeightRef = React.useRef(bottomTerminalHeight || 300);
+ const previousHeightRef = React.useRef(bottomTerminalHeight || 300);
+
+ const standardHeight = React.useMemo(
+ () => Math.min(BOTTOM_DOCK_MAX_HEIGHT, Math.max(BOTTOM_DOCK_MIN_HEIGHT, bottomTerminalHeight || 300)),
+ [bottomTerminalHeight],
+ );
+
+ React.useEffect(() => {
+ if (!isOpen) {
+ setFullscreenHeight(null);
+ setIsResizing(false);
+ }
+ }, [isOpen]);
+
+ React.useEffect(() => {
+ if (isMobile || !isOpen || !isFullscreen) {
+ return;
+ }
+
+ const updateFullscreenHeight = () => {
+ const parentHeight = dockRef.current?.parentElement?.getBoundingClientRect().height;
+ if (!parentHeight || parentHeight <= 0) {
+ return;
+ }
+ const next = Math.round(parentHeight);
+ setFullscreenHeight((prev) => (prev === next ? prev : next));
+ };
+
+ updateFullscreenHeight();
+
+ const parent = dockRef.current?.parentElement;
+ if (!parent) {
+ return;
+ }
+
+ const observer = new ResizeObserver(updateFullscreenHeight);
+ observer.observe(parent);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [isFullscreen, isMobile, isOpen]);
+
+ React.useEffect(() => {
+ if (isMobile || !isResizing || isFullscreen) {
+ return;
+ }
+
+ const handlePointerMove = (event: PointerEvent) => {
+ const delta = startYRef.current - event.clientY;
+ const nextHeight = Math.min(
+ BOTTOM_DOCK_MAX_HEIGHT,
+ Math.max(BOTTOM_DOCK_MIN_HEIGHT, startHeightRef.current + delta)
+ );
+ setBottomTerminalHeight(nextHeight);
+ };
+
+ const handlePointerUp = () => {
+ setIsResizing(false);
+ const latestState = useUIStore.getState();
+ if (latestState.bottomTerminalHeight <= BOTTOM_DOCK_COLLAPSE_THRESHOLD) {
+ setBottomTerminalOpen(false);
+ }
+ };
+
+ window.addEventListener('pointermove', handlePointerMove);
+ window.addEventListener('pointerup', handlePointerUp, { once: true });
+
+ return () => {
+ window.removeEventListener('pointermove', handlePointerMove);
+ window.removeEventListener('pointerup', handlePointerUp);
+ };
+ }, [isFullscreen, isMobile, isResizing, setBottomTerminalHeight, setBottomTerminalOpen]);
+
+ if (isMobile) {
+ return null;
+ }
+
+ const appliedHeight = isOpen
+ ? (isFullscreen ? Math.max(standardHeight, fullscreenHeight ?? standardHeight) : standardHeight)
+ : 0;
+
+ const handlePointerDown = (event: React.PointerEvent) => {
+ if (!isOpen || isFullscreen) return;
+ setIsResizing(true);
+ startYRef.current = event.clientY;
+ startHeightRef.current = appliedHeight;
+ event.preventDefault();
+ };
+
+ const toggleFullscreen = () => {
+ if (!isOpen) return;
+
+ if (isFullscreen) {
+ setBottomTerminalExpanded(false);
+ const restoreHeight = Math.min(BOTTOM_DOCK_MAX_HEIGHT, Math.max(BOTTOM_DOCK_MIN_HEIGHT, previousHeightRef.current));
+ setBottomTerminalHeight(restoreHeight);
+ return;
+ }
+
+ previousHeightRef.current = standardHeight;
+ setBottomTerminalExpanded(true);
+ };
+
+ return (
+
+ {isOpen && !isFullscreen && (
+
+ )}
+
+ {isOpen && (
+
+
+ {isFullscreen ?