Compare commits
37 Commits
v0.2.6-dev
...
v0.2.7-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a1aee8f91 | ||
|
|
5384ff8e80 | ||
|
|
6d5836ce1f | ||
|
|
d3dc170e02 | ||
|
|
983c8cc4a3 | ||
|
|
757c587b17 | ||
|
|
5f9cf397b9 | ||
|
|
78ab17d148 | ||
|
|
e91923ad99 | ||
|
|
fd23ea54b6 | ||
|
|
1e7969eaba | ||
|
|
77bfe41a8e | ||
|
|
6d134e4dec | ||
|
|
9423326193 | ||
|
|
c5011e4ece | ||
|
|
66c270151a | ||
|
|
5ce41217e9 | ||
|
|
1e4d949d35 | ||
|
|
6bb9e8e414 | ||
|
|
1efc49b67b | ||
|
|
f0ed98222a | ||
|
|
ddd8ce341a | ||
|
|
b7721ba3e7 | ||
|
|
0554018980 | ||
|
|
ca18942bfd | ||
|
|
c9c1f69b82 | ||
|
|
aa0c31fa1e | ||
|
|
96b88dbcdc | ||
|
|
50676416ed | ||
|
|
f633d75005 | ||
|
|
4085f6d6b9 | ||
|
|
ae288833e1 | ||
|
|
f16e244265 | ||
|
|
b6e43c899b | ||
|
|
9fa436b0b8 | ||
|
|
ccd65fbc74 | ||
|
|
daa7e3a6d1 |
4
.github/workflows/dev-release.yml
vendored
4
.github/workflows/dev-release.yml
vendored
@@ -57,7 +57,9 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
publish-server:
|
||||
needs: prepare-dev
|
||||
needs:
|
||||
- prepare-dev
|
||||
- build-and-upload
|
||||
uses: ./.github/workflows/manual-npm-publish.yml
|
||||
with:
|
||||
version: ${{ needs.prepare-dev.outputs.version }}
|
||||
|
||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -77,6 +77,7 @@ jobs:
|
||||
needs:
|
||||
- prepare-release
|
||||
- build-and-upload
|
||||
if: ${{ needs.build-and-upload.result == 'success' }}
|
||||
uses: ./.github/workflows/manual-npm-publish.yml
|
||||
with:
|
||||
version: ${{ needs.prepare-release.outputs.version }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ release/
|
||||
.vite/
|
||||
.electron-vite/
|
||||
out/
|
||||
.dir-locals.el
|
||||
82
dev-docs/solidjs-llms.txt
Normal file
82
dev-docs/solidjs-llms.txt
Normal file
@@ -0,0 +1,82 @@
|
||||
# SolidJS Documentation
|
||||
|
||||
> Solid is a modern JavaScript framework for building user interfaces with fine-grained reactivity. It compiles JSX to real DOM elements and updates only what changes, delivering exceptional performance without a virtual DOM. Solid provides reactive primitives like signals, effects, and stores for predictable state management.
|
||||
|
||||
SolidJS is a declarative JavaScript framework that prioritizes performance and developer experience. Unlike frameworks that re-run components on every update, Solid components run once during initialization and set up a reactive system that precisely updates the DOM when dependencies change.
|
||||
|
||||
Key principles:
|
||||
- Fine-grained reactivity: Updates only the specific DOM nodes that depend on changed data
|
||||
- Compile-time optimization: JSX transforms into efficient DOM operations
|
||||
- Unidirectional data flow: Props are read-only, promoting predictable state management
|
||||
- Component lifecycle: Components run once, with reactive primitives handling updates
|
||||
|
||||
**Use your web fetch tool on any of the following links to understand the relevant concept**.
|
||||
|
||||
## Quick Start
|
||||
|
||||
- [Overview](https://docs.solidjs.com/): Framework introduction and key advantages
|
||||
- [Quick Start](https://docs.solidjs.com/quick-start): Installation and project setup with create-solid
|
||||
- [Interactive Tutorial](https://www.solidjs.com/tutorial/introduction_basics): Learn Solid basics through guided examples
|
||||
- [Playground](https://playground.solidjs.com/): Experiment with Solid directly in your browser
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- [Intro to Reactivity](https://docs.solidjs.com/concepts/intro-to-reactivity): Signals, subscribers, and reactive principles
|
||||
- [Understanding JSX](https://docs.solidjs.com/concepts/understanding-jsx): How Solid uses JSX and key differences from HTML
|
||||
- [Components Basics](https://docs.solidjs.com/concepts/components/basics): Component trees, lifecycles, and composition patterns
|
||||
- [Signals](https://docs.solidjs.com/concepts/signals): Core reactive primitive for state management with getters/setters
|
||||
- [Effects](https://docs.solidjs.com/concepts/effects): Side effects, dependency tracking, and lifecycle functions
|
||||
- [Stores](https://docs.solidjs.com/concepts/stores): Complex state management with proxy-based reactivity
|
||||
- [Context](https://docs.solidjs.com/concepts/context): Cross-component state sharing without prop drilling
|
||||
|
||||
## Component APIs
|
||||
|
||||
- [Props](https://docs.solidjs.com/concepts/components/props): Passing data and handlers to child components
|
||||
- [Event Handlers](https://docs.solidjs.com/concepts/components/event-handlers): Managing user interactions
|
||||
- [Class and Style](https://docs.solidjs.com/concepts/components/class-style): Dynamic styling approaches
|
||||
- [Refs](https://docs.solidjs.com/concepts/refs): Accessing DOM elements directly
|
||||
|
||||
## Control Flow
|
||||
|
||||
- [Conditional Rendering](https://docs.solidjs.com/concepts/control-flow/conditional-rendering): Show, Switch, and Match components
|
||||
- [List Rendering](https://docs.solidjs.com/concepts/control-flow/list-rendering): For, Index, and keyed iteration
|
||||
- [Dynamic](https://docs.solidjs.com/concepts/control-flow/dynamic): Dynamic component switching
|
||||
- [Portal](https://docs.solidjs.com/concepts/control-flow/portal): Rendering outside component hierarchy
|
||||
- [Error Boundary](https://docs.solidjs.com/concepts/control-flow/error-boundary): Graceful error handling
|
||||
|
||||
## Derived Values
|
||||
|
||||
- [Derived Signals](https://docs.solidjs.com/concepts/derived-values/derived-signals): Computed values from signals
|
||||
- [Memos](https://docs.solidjs.com/concepts/derived-values/memos): Cached computed values for performance
|
||||
|
||||
## State Management
|
||||
|
||||
- [Basic State Management](https://docs.solidjs.com/guides/state-management): One-way data flow and lifting state
|
||||
- [Complex State Management](https://docs.solidjs.com/guides/complex-state-management): Stores for scalable applications
|
||||
- [Fetching Data](https://docs.solidjs.com/guides/fetching-data): Async data with createResource
|
||||
|
||||
## Routing
|
||||
|
||||
- [Routing & Navigation](https://docs.solidjs.com/guides/routing-and-navigation): @solidjs/router setup and usage
|
||||
- [Dynamic Routes](https://docs.solidjs.com/guides/routing-and-navigation#dynamic-routes): Route parameters and validation
|
||||
- [Nested Routes](https://docs.solidjs.com/guides/routing-and-navigation#nested-routes): Hierarchical route structures
|
||||
- [Preload Functions](https://docs.solidjs.com/guides/routing-and-navigation#preload-functions): Parallel data fetching
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
- [Fine-Grained Reactivity](https://docs.solidjs.com/advanced-concepts/fine-grained-reactivity): Deep dive into reactive system
|
||||
- [TypeScript](https://docs.solidjs.com/configuration/typescript): Type safety and configuration
|
||||
|
||||
## Ecosystem
|
||||
|
||||
- [Solid Router](https://docs.solidjs.com/solid-router/): File-system routing and data APIs
|
||||
- [SolidStart](https://docs.solidjs.com/solid-start/): Full-stack meta-framework
|
||||
- [Solid Meta](https://docs.solidjs.com/solid-meta/): Document head management
|
||||
- [Templates](https://github.com/solidjs/templates): Starter templates for different setups
|
||||
|
||||
## Optional
|
||||
|
||||
- [Ecosystem Libraries](https://www.solidjs.com/ecosystem): Community packages and tools
|
||||
- [API Reference](https://docs.solidjs.com/reference/): Complete API documentation
|
||||
- [Testing](https://docs.solidjs.com/guides/testing): Testing strategies and utilities
|
||||
- [Deployment](https://docs.solidjs.com/guides/deploying-your-app): Build and deployment options
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"dependencies": {
|
||||
"7zip-bin": "^5.2.0",
|
||||
"google-auth-library": "^10.5.0"
|
||||
@@ -8613,7 +8613,7 @@
|
||||
},
|
||||
"packages/electron-app": {
|
||||
"name": "@neuralnomads/codenomad-electron-app",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"dependencies": {
|
||||
"@codenomad/ui": "file:../ui",
|
||||
"@neuralnomads/codenomad": "file:../server"
|
||||
@@ -8641,7 +8641,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.5.0",
|
||||
"@fastify/reply-from": "^9.8.0",
|
||||
@@ -8680,14 +8680,14 @@
|
||||
},
|
||||
"packages/tauri-app": {
|
||||
"name": "@codenomad/tauri-app",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.9.4"
|
||||
}
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@codenomad/ui",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"dependencies": {
|
||||
"@git-diff-view/solid": "^0.0.8",
|
||||
"@kobalte/core": "0.13.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"private": true,
|
||||
"description": "CodeNomad monorepo workspace",
|
||||
"workspaces": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neuralnomads/codenomad-electron-app",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"description": "CodeNomad - AI coding assistant",
|
||||
"author": {
|
||||
"name": "Neural Nomads",
|
||||
|
||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.5.0",
|
||||
"commander": "^12.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"description": "CodeNomad Server",
|
||||
"author": {
|
||||
"name": "Neural Nomads",
|
||||
|
||||
@@ -18,6 +18,7 @@ const PreferencesSchema = z.object({
|
||||
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||
showUsageMetrics: z.boolean().default(true),
|
||||
autoCleanupBlankSessions: z.boolean().default(true),
|
||||
})
|
||||
|
||||
const RecentFolderSchema = z.object({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@codenomad/tauri-app",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npx --yes @tauri-apps/cli@^2.9.4 dev",
|
||||
|
||||
@@ -16,9 +16,9 @@ const sources = ["dist", "public", "node_modules", "package.json"]
|
||||
const serverInstallCommand =
|
||||
"npm install --omit=dev --ignore-scripts --workspaces=false --package-lock=false --install-strategy=shallow --fund=false --audit=false"
|
||||
const serverDevInstallCommand =
|
||||
"npm ci --workspace @neuralnomads/codenomad --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
|
||||
"npm install --workspace @neuralnomads/codenomad --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
|
||||
const uiDevInstallCommand =
|
||||
"npm ci --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
|
||||
"npm install --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
|
||||
|
||||
const envWithRootBin = {
|
||||
...process.env,
|
||||
@@ -114,6 +114,42 @@ function ensureUiDevDependencies() {
|
||||
})
|
||||
}
|
||||
|
||||
function ensureRollupPlatformBinary() {
|
||||
const platformKey = `${process.platform}-${process.arch}`
|
||||
const platformPackages = {
|
||||
"linux-x64": "@rollup/rollup-linux-x64-gnu",
|
||||
"linux-arm64": "@rollup/rollup-linux-arm64-gnu",
|
||||
"darwin-arm64": "@rollup/rollup-darwin-arm64",
|
||||
"darwin-x64": "@rollup/rollup-darwin-x64",
|
||||
"win32-x64": "@rollup/rollup-win32-x64-msvc",
|
||||
}
|
||||
|
||||
const pkgName = platformPackages[platformKey]
|
||||
if (!pkgName) {
|
||||
return
|
||||
}
|
||||
|
||||
const platformPackagePath = path.join(workspaceRoot, "node_modules", "@rollup", pkgName.split("/").pop())
|
||||
if (fs.existsSync(platformPackagePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
let rollupVersion = ""
|
||||
try {
|
||||
rollupVersion = require(path.join(workspaceRoot, "node_modules", "rollup", "package.json")).version
|
||||
} catch (error) {
|
||||
// leave version empty; fallback install will use latest compatible
|
||||
}
|
||||
|
||||
const packageSpec = rollupVersion ? `${pkgName}@${rollupVersion}` : pkgName
|
||||
|
||||
console.log("[prebuild] installing rollup platform binary (optional dep workaround)...")
|
||||
execSync(`npm install ${packageSpec} --no-save --ignore-scripts --fund=false --audit=false`, {
|
||||
cwd: workspaceRoot,
|
||||
stdio: "inherit",
|
||||
})
|
||||
}
|
||||
|
||||
function copyServerArtifacts() {
|
||||
fs.rmSync(serverDest, { recursive: true, force: true })
|
||||
fs.mkdirSync(serverDest, { recursive: true })
|
||||
@@ -151,8 +187,9 @@ function copyUiLoadingAssets() {
|
||||
|
||||
ensureServerDevDependencies()
|
||||
ensureUiDevDependencies()
|
||||
ensureRollupPlatformBinary()
|
||||
ensureServerDependencies()
|
||||
ensureServerBuild()
|
||||
ensureUiBuild()
|
||||
ensureServerDependencies()
|
||||
copyServerArtifacts()
|
||||
copyUiLoadingAssets()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@codenomad/ui",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -47,6 +47,7 @@ const App: Component = () => {
|
||||
preferences,
|
||||
recordWorkspaceLaunch,
|
||||
toggleShowThinkingBlocks,
|
||||
toggleAutoCleanupBlankSessions,
|
||||
toggleUsageMetrics,
|
||||
setDiffViewMode,
|
||||
setToolOutputExpansion,
|
||||
@@ -206,6 +207,7 @@ const App: Component = () => {
|
||||
|
||||
const { commands: paletteCommands, executeCommand } = useCommands({
|
||||
preferences,
|
||||
toggleAutoCleanupBlankSessions,
|
||||
toggleShowThinkingBlocks,
|
||||
toggleUsageMetrics,
|
||||
setDiffViewMode,
|
||||
@@ -248,7 +250,7 @@ const App: Component = () => {
|
||||
<Dialog.Content class="modal-surface w-full max-w-md p-6 flex flex-col gap-6">
|
||||
<div>
|
||||
<Dialog.Title class="text-xl font-semibold text-primary">Unable to launch OpenCode</Dialog.Title>
|
||||
<Dialog.Description class="text-sm text-secondary mt-2">
|
||||
<Dialog.Description class="text-sm text-secondary mt-2 break-words">
|
||||
Install the OpenCode CLI and make sure it is available in your PATH, or pick a custom binary from
|
||||
Advanced Settings.
|
||||
</Dialog.Description>
|
||||
|
||||
@@ -89,9 +89,9 @@ const AlertDialog: Component = () => {
|
||||
>
|
||||
{accent.symbol}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 min-w-0">
|
||||
<Dialog.Title class="text-lg font-semibold text-primary">{title}</Dialog.Title>
|
||||
<Dialog.Description class="text-sm text-secondary mt-1 whitespace-pre-line">
|
||||
<Dialog.Description class="text-sm text-secondary mt-1 whitespace-pre-line break-words">
|
||||
{payload.message}
|
||||
{payload.detail && <p class="mt-2 text-secondary">{payload.detail}</p>}
|
||||
</Dialog.Description>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createMemo, Show, createEffect, onCleanup } from "solid-js"
|
||||
import { DiffView, DiffModeEnum } from "@git-diff-view/solid"
|
||||
import type { DiffHighlighterLang } from "@git-diff-view/core"
|
||||
import { ErrorBoundary } from "solid-js"
|
||||
import { getLanguageFromPath } from "../lib/markdown"
|
||||
import { normalizeDiffText } from "../lib/diff-utils"
|
||||
import { setCacheEntry } from "../lib/global-cache"
|
||||
@@ -36,10 +37,10 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const language = getLanguageFromPath(props.filePath) || "text"
|
||||
const fileName = props.filePath || "diff"
|
||||
|
||||
|
||||
return {
|
||||
oldFile: {
|
||||
fileName,
|
||||
@@ -52,96 +53,47 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
||||
hunks: [normalized],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
let diffContainerRef: HTMLDivElement | undefined
|
||||
let pendingCapture: number | undefined
|
||||
let pendingContext: CaptureContext | undefined
|
||||
let lastRenderedMarkup: string | undefined
|
||||
let lastCachedHtml: string | undefined
|
||||
|
||||
const clearPendingCapture = () => {
|
||||
if (pendingCapture !== undefined) {
|
||||
cancelAnimationFrame(pendingCapture)
|
||||
pendingCapture = undefined
|
||||
}
|
||||
pendingContext = undefined
|
||||
}
|
||||
|
||||
const runCapture = (context: CaptureContext) => {
|
||||
if (!diffContainerRef) {
|
||||
props.onRendered?.()
|
||||
return
|
||||
}
|
||||
|
||||
const markup = diffContainerRef.innerHTML
|
||||
if (!markup) {
|
||||
props.onRendered?.()
|
||||
return
|
||||
}
|
||||
|
||||
const hasChanged = markup !== lastRenderedMarkup
|
||||
if (hasChanged) {
|
||||
lastRenderedMarkup = markup
|
||||
if (context.cacheEntryParams) {
|
||||
setCacheEntry(context.cacheEntryParams, {
|
||||
text: context.diffText,
|
||||
html: markup,
|
||||
theme: context.theme,
|
||||
mode: context.mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
props.onRendered?.()
|
||||
}
|
||||
|
||||
const scheduleCapture = (context: CaptureContext) => {
|
||||
clearPendingCapture()
|
||||
pendingContext = context
|
||||
pendingCapture = requestAnimationFrame(() => {
|
||||
const activeContext = pendingContext
|
||||
pendingContext = undefined
|
||||
pendingCapture = undefined
|
||||
if (activeContext) {
|
||||
runCapture(activeContext)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let lastCapturedKey: string | undefined
|
||||
|
||||
const contextKey = createMemo(() => {
|
||||
const data = diffData()
|
||||
if (!data) return ""
|
||||
return `${props.theme}|${props.mode}|${props.diffText}`
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const cachedHtml = props.cachedHtml
|
||||
if (cachedHtml) {
|
||||
clearPendingCapture()
|
||||
if (cachedHtml !== lastCachedHtml) {
|
||||
lastCachedHtml = cachedHtml
|
||||
lastRenderedMarkup = cachedHtml
|
||||
props.onRendered?.()
|
||||
// When we are given cached HTML, we rely on the caller's cache
|
||||
// and simply notify once rendered.
|
||||
props.onRendered?.()
|
||||
return
|
||||
}
|
||||
|
||||
const key = contextKey()
|
||||
if (!key) return
|
||||
if (!diffContainerRef) return
|
||||
if (lastCapturedKey === key) return
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!diffContainerRef) return
|
||||
const markup = diffContainerRef.innerHTML
|
||||
if (!markup) return
|
||||
lastCapturedKey = key
|
||||
if (props.cacheEntryParams) {
|
||||
setCacheEntry(props.cacheEntryParams, {
|
||||
text: props.diffText,
|
||||
html: markup,
|
||||
theme: props.theme,
|
||||
mode: props.mode,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lastCachedHtml = undefined
|
||||
|
||||
const data = diffData()
|
||||
const theme = props.theme
|
||||
const mode = props.mode
|
||||
|
||||
if (!data) {
|
||||
clearPendingCapture()
|
||||
return
|
||||
}
|
||||
|
||||
scheduleCapture({
|
||||
theme,
|
||||
mode,
|
||||
diffText: props.diffText,
|
||||
cacheEntryParams: props.cacheEntryParams,
|
||||
props.onRendered?.()
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
clearPendingCapture()
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="tool-call-diff-viewer">
|
||||
@@ -154,14 +106,19 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
||||
fallback={<pre class="tool-call-diff-fallback">{props.diffText}</pre>}
|
||||
>
|
||||
{(data) => (
|
||||
<DiffView
|
||||
data={data()}
|
||||
diffViewMode={props.mode === "split" ? DiffModeEnum.Split : DiffModeEnum.Unified}
|
||||
diffViewTheme={props.theme}
|
||||
diffViewHighlight
|
||||
diffViewWrap={false}
|
||||
diffViewFontSize={13}
|
||||
/>
|
||||
<ErrorBoundary fallback={(error) => {
|
||||
console.warn("Failed to render diff view", error)
|
||||
return <pre class="tool-call-diff-fallback">{props.diffText}</pre>
|
||||
}}>
|
||||
<DiffView
|
||||
data={data()}
|
||||
diffViewMode={props.mode === "split" ? DiffModeEnum.Split : DiffModeEnum.Unified}
|
||||
diffViewTheme={props.theme}
|
||||
diffViewHighlight
|
||||
diffViewWrap={false}
|
||||
diffViewFontSize={13}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function InstanceDisconnectedModal(props: InstanceDisconnectedMod
|
||||
<Dialog.Content class="modal-surface w-full max-w-md p-6 flex flex-col gap-6">
|
||||
<div>
|
||||
<Dialog.Title class="text-xl font-semibold text-primary">Instance Disconnected</Dialog.Title>
|
||||
<Dialog.Description class="text-sm text-secondary mt-2">
|
||||
<Dialog.Description class="text-sm text-secondary mt-2 break-words">
|
||||
{folderLabel} can no longer be reached. Close the tab to continue working.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,8 @@ export function Markdown(props: MarkdownProps) {
|
||||
|
||||
latestRequestedText = text
|
||||
|
||||
await initMarkdown(dark)
|
||||
// Markdown initialization is now handled globally in App.
|
||||
// initMarkdown is idempotent but we avoid per-part calls here.
|
||||
|
||||
if (!highlightEnabled) {
|
||||
part.renderCache = undefined
|
||||
|
||||
@@ -10,31 +10,36 @@ interface MessageItemProps {
|
||||
instanceId: string
|
||||
sessionId: string
|
||||
isQueued?: boolean
|
||||
combinedParts: ClientPart[]
|
||||
orderedParts: ClientPart[]
|
||||
parts: ClientPart[]
|
||||
onRevert?: (messageId: string) => void
|
||||
onFork?: (messageId?: string) => void
|
||||
showAgentMeta?: boolean
|
||||
}
|
||||
onContentRendered?: () => void
|
||||
}
|
||||
|
||||
export default function MessageItem(props: MessageItemProps) {
|
||||
|
||||
export default function MessageItem(props: MessageItemProps) {
|
||||
const isUser = () => props.record.role === "user"
|
||||
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
|
||||
|
||||
const timestamp = () => {
|
||||
const createdTime = props.messageInfo?.time?.created ?? props.record.createdAt
|
||||
const date = new Date(createdTime)
|
||||
const date = new Date(createdTimestamp())
|
||||
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
||||
}
|
||||
|
||||
const timestampIso = () => new Date(createdTimestamp()).toISOString()
|
||||
|
||||
type FilePart = Extract<ClientPart, { type: "file" }> & {
|
||||
url?: string
|
||||
mime?: string
|
||||
filename?: string
|
||||
}
|
||||
|
||||
const combinedParts = () => props.combinedParts
|
||||
|
||||
const messageParts = () => props.parts
|
||||
|
||||
const fileAttachments = () =>
|
||||
props.orderedParts.filter((part): part is FilePart => part?.type === "file" && typeof (part as FilePart).url === "string")
|
||||
messageParts().filter((part): part is FilePart => part?.type === "file" && typeof (part as FilePart).url === "string")
|
||||
|
||||
|
||||
const getAttachmentName = (part: FilePart) => {
|
||||
if (part.filename && part.filename.trim().length > 0) {
|
||||
@@ -124,7 +129,7 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
return true
|
||||
}
|
||||
|
||||
return combinedParts().some((part) => partHasRenderableText(part))
|
||||
return messageParts().some((part) => partHasRenderableText(part))
|
||||
}
|
||||
|
||||
const isGenerating = () => {
|
||||
@@ -147,6 +152,8 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
? "message-item-base bg-[var(--message-user-bg)] border-l-4 border-[var(--message-user-border)]"
|
||||
: "message-item-base assistant-message bg-[var(--message-assistant-bg)] border-l-4 border-[var(--message-assistant-border)]"
|
||||
|
||||
const speakerLabel = () => (isUser() ? "You" : "Assistant")
|
||||
|
||||
const agentIdentifier = () => {
|
||||
if (isUser()) return ""
|
||||
const info = props.messageInfo
|
||||
@@ -164,55 +171,63 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
return modelID
|
||||
}
|
||||
|
||||
const agentMeta = () => {
|
||||
if (isUser() || !props.showAgentMeta) return ""
|
||||
const segments: string[] = []
|
||||
const agent = agentIdentifier()
|
||||
const model = modelIdentifier()
|
||||
if (agent) {
|
||||
segments.push(`Agent: ${agent}`)
|
||||
}
|
||||
if (model) {
|
||||
segments.push(`Model: ${model}`)
|
||||
}
|
||||
return segments.join(" • ")
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div class={containerClass()}>
|
||||
<div class={`flex justify-between items-center gap-2.5 ${isUser() ? "pb-0.5" : "pb-0"}`}>
|
||||
<div class="flex flex-col">
|
||||
<Show when={isUser()}>
|
||||
<span class="font-semibold text-xs text-[var(--message-user-border)]">You</span>
|
||||
</Show>
|
||||
<Show when={!isUser()}>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-[var(--message-assistant-border)]">
|
||||
<span class="font-semibold">Assistant</span>
|
||||
<Show when={props.showAgentMeta && (agentIdentifier() || modelIdentifier())}>
|
||||
<span class="message-step-meta-inline">
|
||||
<Show when={agentIdentifier()}>{(value) => <span class="font-medium text-[var(--message-assistant-border)]">Agent: {value()}</span>}</Show>
|
||||
<Show when={modelIdentifier()}>{(value) => <span class="font-medium text-[var(--message-assistant-border)]">Model: {value()}</span>}</Show>
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
<Show when={isUser() && props.onRevert}>
|
||||
<button
|
||||
class="bg-transparent border border-[var(--border-base)] text-[var(--text-muted)] cursor-pointer px-3 py-0.5 rounded text-xs font-semibold leading-none transition-all duration-200 flex items-center justify-center h-6 hover:bg-[var(--surface-hover)] hover:border-[var(--accent-primary)] hover:text-[var(--accent-primary)] active:scale-95"
|
||||
onClick={handleRevert}
|
||||
title="Revert to this message"
|
||||
aria-label="Revert to this message"
|
||||
>
|
||||
Revert to
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={isUser() && props.onFork}>
|
||||
<button
|
||||
class="bg-transparent border border-[var(--border-base)] text-[var(--text-muted)] cursor-pointer px-3 py-0.5 rounded text-xs font-semibold leading-none transition-all duration-200 flex items-center justify-center h-6 hover:bg-[var(--surface-hover)] hover:border-[var(--accent-primary)] hover:text-[var(--accent-primary)] active:scale-95"
|
||||
onClick={() => props.onFork?.(props.record.id)}
|
||||
title="Fork from this message"
|
||||
aria-label="Fork from this message"
|
||||
>
|
||||
Fork
|
||||
</button>
|
||||
</Show>
|
||||
<span class="text-[11px] text-[var(--text-muted)]">{timestamp()}</span>
|
||||
<div class={containerClass()}>
|
||||
<header class={`message-item-header ${isUser() ? "pb-0.5" : "pb-0"}`}>
|
||||
<div class="message-speaker">
|
||||
<span class="message-speaker-label" data-role={isUser() ? "user" : "assistant"}>
|
||||
{speakerLabel()}
|
||||
</span>
|
||||
<Show when={agentMeta()}>{(meta) => <span class="message-agent-meta">{meta()}</span>}</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-item-actions">
|
||||
<Show when={isUser()}>
|
||||
<div class="message-action-group">
|
||||
<Show when={props.onRevert}>
|
||||
<button
|
||||
class="message-action-button"
|
||||
onClick={handleRevert}
|
||||
title="Revert to this message"
|
||||
aria-label="Revert to this message"
|
||||
>
|
||||
Revert
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={props.onFork}>
|
||||
<button
|
||||
class="message-action-button"
|
||||
onClick={() => props.onFork?.(props.record.id)}
|
||||
title="Fork from this message"
|
||||
aria-label="Fork from this message"
|
||||
>
|
||||
Fork
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="pt-1 whitespace-pre-wrap break-words leading-[1.1]">
|
||||
|
||||
|
||||
<Show when={props.isQueued && isUser()}>
|
||||
<div class="message-queued-badge">QUEUED</div>
|
||||
</Show>
|
||||
@@ -227,13 +242,14 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<For each={combinedParts()}>
|
||||
<For each={messageParts()}>
|
||||
{(part) => (
|
||||
<MessagePart
|
||||
part={part}
|
||||
messageType={props.record.role}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
onRendered={props.onContentRendered}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -13,8 +13,10 @@ interface MessagePartProps {
|
||||
messageType?: "user" | "assistant"
|
||||
instanceId: string
|
||||
sessionId: string
|
||||
}
|
||||
export default function MessagePart(props: MessagePartProps) {
|
||||
onRendered?: () => void
|
||||
}
|
||||
export default function MessagePart(props: MessagePartProps) {
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const { preferences } = useConfig()
|
||||
const partType = () => props.part?.type || ""
|
||||
@@ -95,11 +97,17 @@ export default function MessagePart(props: MessagePartProps) {
|
||||
<Show when={!(props.part.type === "text" && props.part.synthetic) && partHasRenderableText(props.part)}>
|
||||
<div class={textContainerClass()}>
|
||||
<Show
|
||||
when={isAssistantMessage()}
|
||||
fallback={<span>{plainTextContent()}</span>}
|
||||
>
|
||||
<Markdown part={createTextPartForMarkdown()} isDark={isDark()} size={isAssistantMessage() ? "tight" : "base"} />
|
||||
</Show>
|
||||
when={isAssistantMessage()}
|
||||
fallback={<span>{plainTextContent()}</span>}
|
||||
>
|
||||
<Markdown
|
||||
part={createTextPartForMarkdown()}
|
||||
isDark={isDark()}
|
||||
size={isAssistantMessage() ? "tight" : "base"}
|
||||
onRendered={props.onRendered}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
</Show>
|
||||
</Match>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,8 +25,10 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
const session = () => props.activeSessions.get(props.sessionId)
|
||||
const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId))
|
||||
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
|
||||
let scrollToBottomHandle: (() => void) | undefined
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
createEffect(() => {
|
||||
const currentSession = session()
|
||||
if (currentSession) {
|
||||
loadMessages(props.instanceId, currentSession.id).catch(console.error)
|
||||
@@ -34,6 +36,9 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
})
|
||||
|
||||
async function handleSendMessage(prompt: string, attachments: Attachment[]) {
|
||||
if (scrollToBottomHandle) {
|
||||
scrollToBottomHandle()
|
||||
}
|
||||
await sendMessage(props.instanceId, props.sessionId, prompt, attachments)
|
||||
}
|
||||
|
||||
@@ -137,12 +142,16 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
return (
|
||||
<div class="session-view">
|
||||
<MessageStreamV2
|
||||
instanceId={props.instanceId}
|
||||
sessionId={activeSession.id}
|
||||
loading={messagesLoading()}
|
||||
onRevert={handleRevert}
|
||||
onFork={handleFork}
|
||||
/>
|
||||
instanceId={props.instanceId}
|
||||
sessionId={activeSession.id}
|
||||
loading={messagesLoading()}
|
||||
onRevert={handleRevert}
|
||||
onFork={handleFork}
|
||||
registerScrollToBottom={(fn) => {
|
||||
scrollToBottomHandle = fn
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
<PromptInput
|
||||
instanceId={props.instanceId}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createSignal, Show, For, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { isToolCallExpanded, toggleToolCallExpanded, setToolCallExpanded } from "../stores/tool-call-state"
|
||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||
import { Markdown } from "./markdown"
|
||||
import { ToolCallDiffViewer } from "./diff-viewer"
|
||||
@@ -57,7 +56,9 @@ interface ToolCallProps {
|
||||
partVersion?: number
|
||||
instanceId: string
|
||||
sessionId: string
|
||||
}
|
||||
onContentRendered?: () => void
|
||||
}
|
||||
|
||||
|
||||
function getToolIcon(tool: string): string {
|
||||
switch (tool) {
|
||||
@@ -349,10 +350,28 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
}
|
||||
return props.toolCall.pendingPermission
|
||||
})
|
||||
const expanded = () => (pendingPermission() ? true : isToolCallExpanded(toolCallId()))
|
||||
const toolOutputDefaultExpanded = createMemo(() => (preferences().toolOutputExpansion || "expanded") === "expanded")
|
||||
const diagnosticsDefaultExpanded = createMemo(() => (preferences().diagnosticsExpansion || "expanded") === "expanded")
|
||||
const [appliedPreference, setAppliedPreference] = createSignal<boolean | null>(null)
|
||||
|
||||
const defaultExpandedForTool = createMemo(() => {
|
||||
const prefExpanded = toolOutputDefaultExpanded()
|
||||
const toolName = props.toolCall?.tool || ""
|
||||
if (toolName === "read") {
|
||||
return false
|
||||
}
|
||||
return prefExpanded
|
||||
})
|
||||
|
||||
const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null)
|
||||
|
||||
const expanded = () => {
|
||||
const permission = pendingPermission()
|
||||
if (permission?.active) return true
|
||||
const override = userExpanded()
|
||||
if (override !== null) return override
|
||||
return defaultExpandedForTool()
|
||||
}
|
||||
|
||||
const permissionDetails = createMemo(() => pendingPermission()?.permission)
|
||||
const isPermissionActive = createMemo(() => pendingPermission()?.active === true)
|
||||
const activePermissionKey = createMemo(() => {
|
||||
@@ -361,7 +380,16 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
})
|
||||
const [permissionSubmitting, setPermissionSubmitting] = createSignal(false)
|
||||
const [permissionError, setPermissionError] = createSignal<string | null>(null)
|
||||
const [diagnosticsExpanded, setDiagnosticsExpanded] = createSignal(diagnosticsDefaultExpanded())
|
||||
const [diagnosticsOverride, setDiagnosticsOverride] = createSignal<boolean | undefined>(undefined)
|
||||
|
||||
const diagnosticsExpanded = () => {
|
||||
const permission = pendingPermission()
|
||||
if (permission?.active) return true
|
||||
const override = diagnosticsOverride()
|
||||
if (override !== undefined) return override
|
||||
return diagnosticsDefaultExpanded()
|
||||
}
|
||||
|
||||
const diagnosticsEntries = createMemo(() => {
|
||||
const tool = props.toolCall?.tool || ""
|
||||
const state = props.toolCall?.state
|
||||
@@ -369,10 +397,6 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
return extractDiagnostics(tool, state)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const preferred = diagnosticsDefaultExpanded()
|
||||
setDiagnosticsExpanded((prev) => (prev === preferred ? prev : preferred))
|
||||
})
|
||||
|
||||
let scrollContainerRef: HTMLDivElement | undefined
|
||||
let toolCallRootRef: HTMLDivElement | undefined
|
||||
@@ -421,21 +445,6 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
restoreScrollSnapshot(resolvedElement)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const id = toolCallId()
|
||||
if (!id) return
|
||||
const toolName = props.toolCall?.tool || ""
|
||||
const desiredExpansion = toolName === "read" ? false : toolOutputDefaultExpanded()
|
||||
if (appliedPreference() === desiredExpansion) return
|
||||
setToolCallExpanded(id, desiredExpansion)
|
||||
setAppliedPreference(desiredExpansion)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = toolCallId()
|
||||
if (!id) return
|
||||
setAppliedPreference((prev) => (prev === null ? prev : null))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const permission = permissionDetails()
|
||||
@@ -447,19 +456,6 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (props.toolCall?.tool !== "task") return
|
||||
const state = props.toolCall?.state
|
||||
const summarySignature = JSON.stringify(
|
||||
state && (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))
|
||||
? state.metadata?.summary ?? []
|
||||
: []
|
||||
)
|
||||
requestAnimationFrame(() => {
|
||||
void summarySignature
|
||||
handleScrollRendered()
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const activeKey = activePermissionKey()
|
||||
@@ -488,6 +484,11 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
onCleanup(() => document.removeEventListener("keydown", handler))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!expanded()) {
|
||||
scrollContainerRef = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const statusIcon = () => {
|
||||
const status = props.toolCall?.state?.status || ""
|
||||
@@ -516,7 +517,14 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
toggleToolCallExpanded(toolCallId())
|
||||
const permission = pendingPermission()
|
||||
if (permission?.active) {
|
||||
return
|
||||
}
|
||||
setUserExpanded((prev) => {
|
||||
const current = prev === null ? defaultExpandedForTool() : prev
|
||||
return !current
|
||||
})
|
||||
}
|
||||
|
||||
const renderToolAction = () => {
|
||||
@@ -760,8 +768,10 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
if (!options?.disableScrollTracking) {
|
||||
handleScrollRendered()
|
||||
}
|
||||
props.onContentRendered?.()
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
class="message-text tool-call-markdown tool-call-markdown-large tool-call-diff-shell"
|
||||
@@ -825,8 +835,10 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
const handleMarkdownRendered = () => {
|
||||
markdownCache.set(markdownPart.renderCache)
|
||||
handleScrollRendered()
|
||||
props.onContentRendered?.()
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
class={messageClass}
|
||||
@@ -1185,21 +1197,25 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
}}
|
||||
class={`tool-call ${combinedStatusClass()}`}
|
||||
>
|
||||
<button class="tool-call-header" onClick={toggle} aria-expanded={expanded()}>
|
||||
<span class="tool-call-icon">{expanded() ? "▼" : "▶"}</span>
|
||||
<span class="tool-call-emoji">{getToolIcon(toolName())}</span>
|
||||
<span class="tool-call-summary">{renderToolTitle()}</span>
|
||||
<span class="tool-call-status">{statusIcon()}</span>
|
||||
<button
|
||||
class="tool-call-header"
|
||||
onClick={toggle}
|
||||
aria-expanded={expanded()}
|
||||
data-status-icon={statusIcon()}
|
||||
>
|
||||
<span class="tool-call-summary" data-tool-icon={getToolIcon(toolName())}>
|
||||
{renderToolTitle()}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<Show when={expanded()}>
|
||||
{expanded() && (
|
||||
<div class="tool-call-details">
|
||||
{renderToolBody()}
|
||||
|
||||
|
||||
{renderError()}
|
||||
|
||||
|
||||
{renderPermissionBlock()}
|
||||
|
||||
|
||||
<Show when={status() === "pending" && !pendingPermission()}>
|
||||
<div class="tool-call-pending-message">
|
||||
<span class="spinner-small"></span>
|
||||
@@ -1207,13 +1223,17 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
)}
|
||||
|
||||
<Show when={diagnosticsEntries().length}>
|
||||
|
||||
{renderDiagnosticsSection(
|
||||
diagnosticsEntries(),
|
||||
diagnosticsExpanded(),
|
||||
() => setDiagnosticsExpanded((prev) => !prev),
|
||||
() => setDiagnosticsOverride((prev) => {
|
||||
const current = prev === undefined ? diagnosticsDefaultExpanded() : prev
|
||||
return !current
|
||||
}),
|
||||
getToolIcon(toolName()),
|
||||
diagnosticFileName(diagnosticsEntries()),
|
||||
)}
|
||||
|
||||
@@ -16,11 +16,13 @@ import { showAlertDialog } from "../../stores/alerts"
|
||||
import type { Instance } from "../../types/instance"
|
||||
import type { MessageRecord } from "../../stores/message-v2/types"
|
||||
import { messageStoreBus } from "../../stores/message-v2/bus"
|
||||
import { cleanupBlankSessions } from "../../stores/session-state"
|
||||
|
||||
export interface UseCommandsOptions {
|
||||
preferences: Accessor<Preferences>
|
||||
toggleShowThinkingBlocks: () => void
|
||||
toggleUsageMetrics: () => void
|
||||
toggleAutoCleanupBlankSessions: () => void
|
||||
setDiffViewMode: (mode: "split" | "unified") => void
|
||||
setToolOutputExpansion: (mode: ExpansionPreference) => void
|
||||
setDiagnosticsExpansion: (mode: ExpansionPreference) => void
|
||||
@@ -142,6 +144,19 @@ export function useCommands(options: UseCommandsOptions) {
|
||||
},
|
||||
})
|
||||
|
||||
commandRegistry.register({
|
||||
id: "cleanup-blank-sessions",
|
||||
label: "Scrub Sessions",
|
||||
description: "Remove empty sessions, subagent sessions that have completed their primary task, and extraneous forked sessions.",
|
||||
category: "Session",
|
||||
keywords: ["cleanup", "blank", "empty", "sessions", "remove", "delete", "scrub"],
|
||||
action: async () => {
|
||||
const instance = activeInstance()
|
||||
if (!instance) return
|
||||
cleanupBlankSessions(instance.id, undefined, true)
|
||||
},
|
||||
})
|
||||
|
||||
commandRegistry.register({
|
||||
id: "switch-to-info",
|
||||
label: "Instance Info",
|
||||
@@ -467,6 +482,18 @@ export function useCommands(options: UseCommandsOptions) {
|
||||
keywords: ["token", "usage", "cost", "stats"],
|
||||
action: options.toggleUsageMetrics,
|
||||
})
|
||||
|
||||
commandRegistry.register({
|
||||
id: "auto-cleanup-blank-sessions",
|
||||
label: () => {
|
||||
const enabled = options.preferences().autoCleanupBlankSessions
|
||||
return `Auto-Cleanup Blank Sessions · ${enabled ? "Enabled" : "Disabled"}`
|
||||
},
|
||||
description: "Automatically clean up blank sessions when creating new ones",
|
||||
category: "System",
|
||||
keywords: ["auto", "cleanup", "blank", "sessions", "toggle"],
|
||||
action: options.toggleAutoCleanupBlankSessions,
|
||||
})
|
||||
|
||||
commandRegistry.register({
|
||||
id: "help",
|
||||
|
||||
@@ -618,6 +618,13 @@ function mutateToolPartPermission(
|
||||
draft.updatedAt = Date.now()
|
||||
}),
|
||||
)
|
||||
|
||||
// Permission attachment/removal can change the rendered height of the
|
||||
// message list (e.g., permission blocks or diffs), so bump the
|
||||
// session revision to ensure auto-scroll reacts.
|
||||
if (messageRecord.sessionId) {
|
||||
store.setState("sessionRevisions", messageRecord.sessionId, (value: number = 0) => value + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function attachPermissionToToolPart(instanceId: string, permission: Permission, active: boolean): void {
|
||||
|
||||
@@ -46,36 +46,13 @@ function ensurePartId(messageId: string, part: ClientPart, index: number): strin
|
||||
const PENDING_PART_MAX_AGE_MS = 30_000
|
||||
|
||||
function clonePart(part: ClientPart): ClientPart {
|
||||
if (!part || typeof part !== "object") {
|
||||
return part
|
||||
}
|
||||
const cloned: Record<string, any> = { ...part }
|
||||
if ("renderCache" in cloned) {
|
||||
cloned.renderCache = undefined
|
||||
}
|
||||
if ("text" in cloned) {
|
||||
cloned.text = cloneStructuredValue(cloned.text)
|
||||
}
|
||||
if ("thinking" in cloned && typeof cloned.thinking === "object") {
|
||||
cloned.thinking = cloneStructuredValue(cloned.thinking)
|
||||
}
|
||||
if ("content" in cloned && Array.isArray(cloned.content)) {
|
||||
cloned.content = cloneStructuredValue(cloned.content)
|
||||
}
|
||||
return cloned as ClientPart
|
||||
// Cloning is intentionally disabled; message parts
|
||||
// are stored as received from the backend.
|
||||
return part
|
||||
}
|
||||
|
||||
function cloneStructuredValue<T>(value: T): T {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => cloneStructuredValue(item)) as T
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
const next: Record<string, any> = {}
|
||||
Object.entries(value as Record<string, any>).forEach(([key, nested]) => {
|
||||
next[key] = cloneStructuredValue(nested)
|
||||
})
|
||||
return next as T
|
||||
}
|
||||
// Legacy helper kept as a no-op to avoid deep copies.
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -462,10 +439,10 @@ export function createInstanceMessageStore(instanceId: string): InstanceMessageS
|
||||
bufferPendingPart({ messageId: input.messageId, part: input.part, receivedAt: Date.now() })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const partId = ensurePartId(input.messageId, input.part, message.partIds.length)
|
||||
const cloned = clonePart(input.part)
|
||||
|
||||
|
||||
setState(
|
||||
"messages",
|
||||
input.messageId,
|
||||
@@ -486,8 +463,13 @@ export function createInstanceMessageStore(instanceId: string): InstanceMessageS
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// Any part update can change the rendered height of the message
|
||||
// list, so we treat it as a session revision for scroll purposes.
|
||||
bumpSessionRevision(message.sessionId)
|
||||
}
|
||||
|
||||
|
||||
function flushPendingParts(messageId: string) {
|
||||
const pending = state.pendingParts[messageId]
|
||||
if (!pending || pending.length === 0) {
|
||||
@@ -636,9 +618,60 @@ export function createInstanceMessageStore(instanceId: string): InstanceMessageS
|
||||
return { entry, active }
|
||||
}
|
||||
|
||||
function pruneMessagesAfterRevert(sessionId: string, revertMessageId: string) {
|
||||
const session = state.sessions[sessionId]
|
||||
if (!session) return
|
||||
const stopIndex = session.messageIds.indexOf(revertMessageId)
|
||||
if (stopIndex === -1) return
|
||||
const removedIds = session.messageIds.slice(stopIndex)
|
||||
const keptIds = session.messageIds.slice(0, stopIndex)
|
||||
if (removedIds.length === 0) return
|
||||
|
||||
setState("sessions", sessionId, "messageIds", keptIds)
|
||||
|
||||
setState("messages", (prev) => {
|
||||
const next = { ...prev }
|
||||
removedIds.forEach((id) => delete next[id])
|
||||
return next
|
||||
})
|
||||
|
||||
setState("messageInfoVersion", (prev) => {
|
||||
const next = { ...prev }
|
||||
removedIds.forEach((id) => delete next[id])
|
||||
return next
|
||||
})
|
||||
|
||||
removedIds.forEach((id) => messageInfoCache.delete(id))
|
||||
|
||||
setState("pendingParts", (prev) => {
|
||||
const next = { ...prev }
|
||||
removedIds.forEach((id) => {
|
||||
if (next[id]) delete next[id]
|
||||
})
|
||||
return next
|
||||
})
|
||||
|
||||
setState("permissions", "byMessage", (prev) => {
|
||||
const next = { ...prev }
|
||||
removedIds.forEach((id) => {
|
||||
if (next[id]) delete next[id]
|
||||
})
|
||||
return next
|
||||
})
|
||||
|
||||
withUsageState(sessionId, (draft) => {
|
||||
removedIds.forEach((id) => removeUsageEntry(draft, id))
|
||||
})
|
||||
|
||||
bumpSessionRevision(sessionId)
|
||||
}
|
||||
|
||||
function setSessionRevert(sessionId: string, revert?: SessionRecord["revert"] | null) {
|
||||
if (!sessionId) return
|
||||
ensureSessionEntry(sessionId)
|
||||
if (revert?.messageID) {
|
||||
pruneMessagesAfterRevert(sessionId, revert.messageID)
|
||||
}
|
||||
setState("sessions", sessionId, "revert", revert ?? null)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface Preferences {
|
||||
toolOutputExpansion: ExpansionPreference
|
||||
diagnosticsExpansion: ExpansionPreference
|
||||
showUsageMetrics: boolean
|
||||
autoCleanupBlankSessions?: boolean
|
||||
}
|
||||
|
||||
export interface OpenCodeBinary {
|
||||
@@ -64,6 +65,7 @@ const defaultPreferences: Preferences = {
|
||||
toolOutputExpansion: "expanded",
|
||||
diagnosticsExpansion: "expanded",
|
||||
showUsageMetrics: true,
|
||||
autoCleanupBlankSessions: true,
|
||||
}
|
||||
|
||||
function deepEqual(a: unknown, b: unknown): boolean {
|
||||
@@ -98,6 +100,7 @@ function normalizePreferences(pref?: Partial<Preferences> & { agentModelSelectio
|
||||
toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultPreferences.toolOutputExpansion,
|
||||
diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultPreferences.diagnosticsExpansion,
|
||||
showUsageMetrics: sanitized.showUsageMetrics ?? defaultPreferences.showUsageMetrics,
|
||||
autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultPreferences.autoCleanupBlankSessions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +288,11 @@ function toggleUsageMetrics(): void {
|
||||
updatePreferences({ showUsageMetrics: !preferences().showUsageMetrics })
|
||||
}
|
||||
|
||||
function toggleAutoCleanupBlankSessions(): void {
|
||||
console.log("toggle auto cleanup")
|
||||
updatePreferences({ autoCleanupBlankSessions: !preferences().autoCleanupBlankSessions })
|
||||
}
|
||||
|
||||
function addRecentFolder(path: string): void {
|
||||
updateConfig((draft) => {
|
||||
draft.recentFolders = buildRecentFolderList(path, draft.recentFolders)
|
||||
@@ -386,6 +394,7 @@ interface ConfigContextValue {
|
||||
updateConfig: typeof updateConfig
|
||||
toggleShowThinkingBlocks: typeof toggleShowThinkingBlocks
|
||||
toggleUsageMetrics: typeof toggleUsageMetrics
|
||||
toggleAutoCleanupBlankSessions: typeof toggleAutoCleanupBlankSessions
|
||||
setDiffViewMode: typeof setDiffViewMode
|
||||
setToolOutputExpansion: typeof setToolOutputExpansion
|
||||
setDiagnosticsExpansion: typeof setDiagnosticsExpansion
|
||||
@@ -418,6 +427,7 @@ const configContextValue: ConfigContextValue = {
|
||||
updateConfig,
|
||||
toggleShowThinkingBlocks,
|
||||
toggleUsageMetrics,
|
||||
toggleAutoCleanupBlankSessions,
|
||||
setDiffViewMode,
|
||||
setToolOutputExpansion,
|
||||
setDiagnosticsExpansion,
|
||||
@@ -473,6 +483,7 @@ export {
|
||||
updateConfig,
|
||||
updatePreferences,
|
||||
toggleShowThinkingBlocks,
|
||||
toggleAutoCleanupBlankSessions,
|
||||
toggleUsageMetrics,
|
||||
recentFolders,
|
||||
addRecentFolder,
|
||||
|
||||
@@ -2,15 +2,17 @@ import type { Session } from "../types/session"
|
||||
import type { Message } from "../types/message"
|
||||
|
||||
import { instances, refreshPermissionsForSession } from "./instances"
|
||||
import { setAgentModelPreference } from "./preferences"
|
||||
import { preferences, setAgentModelPreference } from "./preferences"
|
||||
import { setSessionCompactionState } from "./session-compaction"
|
||||
import {
|
||||
activeSessionId,
|
||||
agents,
|
||||
clearSessionDraftPrompt,
|
||||
getChildSessions,
|
||||
isBlankSession,
|
||||
messagesLoaded,
|
||||
providers,
|
||||
pruneDraftPrompts,
|
||||
providers,
|
||||
setActiveSessionId,
|
||||
setAgents,
|
||||
setMessagesLoaded,
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
sessions,
|
||||
loading,
|
||||
setLoading,
|
||||
cleanupBlankSessions,
|
||||
} from "./session-state"
|
||||
import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models"
|
||||
import { normalizeMessagePart } from "./message-v2/normalizers"
|
||||
@@ -228,6 +231,10 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
||||
return next
|
||||
})
|
||||
|
||||
if (preferences().autoCleanupBlankSessions) {
|
||||
await cleanupBlankSessions(instanceId, session.id)
|
||||
}
|
||||
|
||||
return session
|
||||
} catch (error) {
|
||||
console.error("Failed to create session:", error)
|
||||
|
||||
@@ -81,20 +81,25 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
||||
if (event.type === "message.part.updated") {
|
||||
const rawPart = event.properties?.part
|
||||
if (!rawPart) return
|
||||
|
||||
|
||||
const part = normalizeMessagePart(rawPart)
|
||||
const sessionId = typeof part.sessionID === "string" ? part.sessionID : undefined
|
||||
const messageId = typeof part.messageID === "string" ? part.messageID : undefined
|
||||
const messageInfo = (event as any)?.properties?.message as MessageInfo | undefined
|
||||
|
||||
const fallbackSessionId = typeof messageInfo?.sessionID === "string" ? messageInfo.sessionID : undefined
|
||||
const fallbackMessageId = typeof messageInfo?.id === "string" ? messageInfo.id : undefined
|
||||
|
||||
const sessionId = typeof part.sessionID === "string" ? part.sessionID : fallbackSessionId
|
||||
const messageId = typeof part.messageID === "string" ? part.messageID : fallbackMessageId
|
||||
if (!sessionId || !messageId) return
|
||||
|
||||
|
||||
const session = instanceSessions.get(sessionId)
|
||||
if (!session) return
|
||||
|
||||
|
||||
const store = messageStoreBus.getOrCreate(instanceId)
|
||||
const messageInfo = (event as any)?.properties?.message as MessageInfo | undefined
|
||||
const role: MessageRole = resolveMessageRole(messageInfo)
|
||||
const createdAt = typeof messageInfo?.time?.created === "number" ? messageInfo.time.created : Date.now()
|
||||
|
||||
|
||||
let record = store.getMessage(messageId)
|
||||
if (!record) {
|
||||
const pendingId = findPendingMessageId(store, sessionId, role)
|
||||
@@ -119,8 +124,9 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
||||
if (messageInfo) {
|
||||
upsertMessageInfoV2(instanceId, messageInfo, { status: "streaming" })
|
||||
}
|
||||
|
||||
applyPartUpdateV2(instanceId, { ...part, sessionID: sessionId, messageID: messageId })
|
||||
|
||||
applyPartUpdateV2(instanceId, part)
|
||||
|
||||
updateSessionInfo(instanceId, sessionId)
|
||||
refreshPermissionsForSession(instanceId, sessionId)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
import type { Session, Agent, Provider } from "../types/session"
|
||||
import { deleteSession, loadMessages } from "./session-api"
|
||||
import { showToastNotification } from "../lib/notifications"
|
||||
import { messageStoreBus } from "./message-v2/bus"
|
||||
import { instances } from "./instances"
|
||||
import { showConfirmDialog } from "./alerts"
|
||||
|
||||
export interface SessionInfo {
|
||||
cost: number
|
||||
@@ -221,6 +226,108 @@ function getSessionInfo(instanceId: string, sessionId: string): SessionInfo | un
|
||||
return sessionInfoByInstance().get(instanceId)?.get(sessionId)
|
||||
}
|
||||
|
||||
async function isBlankSession(session: Session, instanceId: string, fetchIfNeeded = false): Promise<boolean> {
|
||||
const created = session.time?.created || 0
|
||||
const updated = session.time?.updated || 0
|
||||
const hasChildren = getChildSessions(instanceId, session.id).length > 0
|
||||
const isFreshSession = created === updated && !hasChildren
|
||||
|
||||
// Common short-circuit: fresh sessions without children
|
||||
if (!fetchIfNeeded) {
|
||||
return isFreshSession
|
||||
}
|
||||
|
||||
// For a more thorough deep clean, we need to look at actual messages
|
||||
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance?.client) {
|
||||
return isFreshSession
|
||||
}
|
||||
let messages: any[] = []
|
||||
try {
|
||||
const response = await instance.client.session.messages({ path: { id: session.id } })
|
||||
messages = response.data || []
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch messages for session ${session.id}:`, error)
|
||||
return isFreshSession
|
||||
}
|
||||
|
||||
// Specific logic by session type
|
||||
if (session.parentId === null) {
|
||||
// Parent: blank if no messages and no children (fresh !== blank sometimes!)
|
||||
const hasChildren = getChildSessions(instanceId, session.id).length > 0
|
||||
return messages.length === 0 && !hasChildren
|
||||
} else if (session.title?.includes("subagent)")) {
|
||||
// Subagent: "blank" (really: finished doing its job) if actually blank...
|
||||
// ... OR no streaming, no pending perms, no tool parts
|
||||
if (messages.length === 0) return true
|
||||
|
||||
const hasStreaming = messages.some((msg) => {
|
||||
const info = msg.info.status || msg.status
|
||||
return info === "streaming" || info === "sending"
|
||||
})
|
||||
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
const lastParts = lastMessage?.parts || []
|
||||
const hasToolPart = lastParts.some((part: any) =>
|
||||
part.type === "tool" || part.data?.type === "tool"
|
||||
)
|
||||
|
||||
return !hasStreaming && !session.pendingPermission && !hasToolPart
|
||||
} else {
|
||||
// Fork: blank if somehow has no messages or at revert point
|
||||
if (messages.length === 0) return true
|
||||
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
const lastInfo = lastMessage?.info || lastMessage
|
||||
return lastInfo?.id === session.revert?.messageID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function cleanupBlankSessions(instanceId: string, excludeSessionId?: string, fetchIfNeeded = false): Promise<void> {
|
||||
const instanceSessions = sessions().get(instanceId)
|
||||
if (!instanceSessions) return
|
||||
|
||||
if (fetchIfNeeded) {
|
||||
const confirmed = await showConfirmDialog(
|
||||
"This cleanup may be slow, and may delete sessions you didn't intend to delete. Are you sure?",
|
||||
{
|
||||
title: "Deep Clean Sessions",
|
||||
detail: "Deep Clean Sessions will delete all sessions that have no messages, remove any finished sub-agent sessions, and clear out any unused forks of a session.",
|
||||
confirmLabel: "Continue",
|
||||
cancelLabel: "Cancel"
|
||||
}
|
||||
)
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
const cleanupPromises = Array.from(instanceSessions)
|
||||
.filter(([sessionId]) => sessionId !== excludeSessionId)
|
||||
.map(async ([sessionId, session]) => {
|
||||
const isBlank = await isBlankSession(session, instanceId, fetchIfNeeded)
|
||||
if (!isBlank) return false
|
||||
|
||||
await deleteSession(instanceId, sessionId).catch((error: Error) => {
|
||||
console.error(`Failed to delete blank session ${sessionId}:`, error)
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
if (cleanupPromises.length > 0) {
|
||||
console.log(`Cleaning up ${cleanupPromises.length} blank sessions`)
|
||||
const deletionResults = await Promise.all(cleanupPromises)
|
||||
const deletedCount = deletionResults.filter(Boolean).length
|
||||
|
||||
if (deletedCount > 0) {
|
||||
showToastNotification({
|
||||
message: `Cleaned up ${deletedCount} blank session${deletedCount === 1 ? "" : "s"}`,
|
||||
variant: "info"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
sessions,
|
||||
setSessions,
|
||||
@@ -259,4 +366,6 @@ export {
|
||||
isSessionBusy,
|
||||
isSessionMessagesLoading,
|
||||
getSessionInfo,
|
||||
isBlankSession,
|
||||
cleanupBlankSessions,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,56 @@
|
||||
@apply flex flex-col gap-2 p-3 w-full;
|
||||
}
|
||||
|
||||
.message-item-header {
|
||||
@apply flex justify-between items-start gap-2.5;
|
||||
}
|
||||
|
||||
.message-speaker {
|
||||
@apply flex flex-col gap-0.5 text-xs;
|
||||
}
|
||||
|
||||
.message-speaker-label {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.message-speaker-label[data-role="user"] {
|
||||
color: var(--message-user-border);
|
||||
}
|
||||
|
||||
.message-speaker-label[data-role="assistant"] {
|
||||
color: var(--message-assistant-border);
|
||||
}
|
||||
|
||||
.message-agent-meta {
|
||||
@apply text-xs text-[var(--message-assistant-border)];
|
||||
}
|
||||
|
||||
.message-item-actions {
|
||||
@apply flex items-center gap-2;
|
||||
}
|
||||
|
||||
.message-action-group {
|
||||
@apply flex items-center gap-2;
|
||||
}
|
||||
|
||||
.message-action-button {
|
||||
@apply bg-transparent border border-[var(--border-base)] text-[var(--text-muted)] cursor-pointer px-3 py-0.5 rounded text-xs font-semibold leading-none transition-all duration-200 flex items-center justify-center h-6;
|
||||
}
|
||||
|
||||
.message-action-button:hover {
|
||||
background-color: var(--surface-hover);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.message-action-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.message-timestamp {
|
||||
@apply text-[11px] text-[var(--text-muted)];
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
/* gap: 0.25rem; */
|
||||
padding: 0.6rem 0.65rem;
|
||||
@@ -35,6 +85,22 @@
|
||||
@apply flex flex-wrap items-center gap-1 text-[10px] text-[var(--text-muted)];
|
||||
}
|
||||
|
||||
.message-step-usage-chip {
|
||||
@apply inline-flex items-center rounded-full border border-[var(--border-base)] px-2 py-0.5 text-[10px];
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.message-step-usage-chip::before {
|
||||
content: attr(data-label);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.message-step-heading {
|
||||
@apply flex flex-wrap items-center gap-2 text-xs;
|
||||
color: var(--text-muted);
|
||||
|
||||
@@ -85,20 +85,46 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.tool-call-header::before {
|
||||
content: "▶";
|
||||
font-size: 11px;
|
||||
margin-right: 0.35rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tool-call-header[aria-expanded="true"]::before {
|
||||
content: "▼";
|
||||
}
|
||||
|
||||
.tool-call-header::after {
|
||||
content: attr(data-status-icon);
|
||||
font-size: 0.95rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-call-header[data-status-icon=""]::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.tool-call-header:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.tool-call-icon {
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
.tool-call-summary {
|
||||
@apply flex-1 text-left;
|
||||
@apply flex-1 text-left inline-flex items-center gap-2;
|
||||
}
|
||||
|
||||
.tool-call-status {
|
||||
@apply text-sm;
|
||||
.tool-call-summary::before {
|
||||
content: attr(data-tool-icon);
|
||||
}
|
||||
|
||||
.tool-call-summary[data-tool-icon=""]::before {
|
||||
margin-right: 0;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.tool-call-summary[data-tool-icon]:not([data-tool-icon=""])::before {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.tool-call-status-success {
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
@apply rounded-lg shadow-2xl flex flex-col;
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-primary);
|
||||
max-width: min(100%, calc(100vw - 32px));
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.modal-search-container {
|
||||
|
||||
Reference in New Issue
Block a user