Compare commits
18 Commits
codenomad/
...
v0.11.5-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9c99e3bd | ||
|
|
1122070b9c | ||
|
|
57b81f00f8 | ||
|
|
362105fe78 | ||
|
|
5834d2df1b | ||
|
|
ef4c8ef425 | ||
|
|
5f755a7e1c | ||
|
|
8607fab5b5 | ||
|
|
0368fe8248 | ||
|
|
b970281fa7 | ||
|
|
8e5a7fc213 | ||
|
|
15f362e8b5 | ||
|
|
7bbd0a1787 | ||
|
|
f8aae56728 | ||
|
|
027d7fc97d | ||
|
|
e90aef4b3c | ||
|
|
e4e89008b2 | ||
|
|
96fe1b86dd |
56
.github/workflows/build-and-upload.yml
vendored
56
.github/workflows/build-and-upload.yml
vendored
@@ -61,7 +61,21 @@ jobs:
|
|||||||
|
|
||||||
- name: Set workspace versions
|
- name: Set workspace versions
|
||||||
if: ${{ inputs.set_versions && inputs.version != '' }}
|
if: ${{ inputs.set_versions && inputs.version != '' }}
|
||||||
run: npm version ${VERSION} --workspaces --include-workspace-root --no-git-tag-version --allow-same-version
|
shell: bash
|
||||||
|
env:
|
||||||
|
NPM_CONFIG_FETCH_RETRIES: 5
|
||||||
|
NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: 20000
|
||||||
|
NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: 120000
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if npm version "${VERSION}" --workspaces --include-workspace-root --no-git-tag-version --allow-same-version; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "npm version failed (attempt $attempt/3); retrying..." >&2
|
||||||
|
sleep $((attempt * 10))
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --workspaces --include=optional
|
run: npm ci --workspaces --include=optional
|
||||||
@@ -72,6 +86,37 @@ jobs:
|
|||||||
- name: Build macOS binaries (Electron)
|
- name: Build macOS binaries (Electron)
|
||||||
run: npm run build:mac --workspace @neuralnomads/codenomad-electron-app
|
run: npm run build:mac --workspace @neuralnomads/codenomad-electron-app
|
||||||
|
|
||||||
|
- name: Ad-hoc sign Electron macOS app bundles (seal resources)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
release_root="packages/electron-app/release"
|
||||||
|
apps=()
|
||||||
|
while IFS= read -r -d '' app; do
|
||||||
|
apps+=("$app")
|
||||||
|
done < <(find "$release_root" -type d -name 'CodeNomad.app' -print0)
|
||||||
|
|
||||||
|
if [ "${#apps[@]}" -eq 0 ]; then
|
||||||
|
echo "No CodeNomad.app found under $release_root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GitHub macOS runners typically have no signing identity. Without any signature,
|
||||||
|
# the shipped .app can fail Gatekeeper with:
|
||||||
|
# code has no resources but signature indicates they must be present
|
||||||
|
# Ad-hoc signing seals bundle resources and makes the signature internally consistent.
|
||||||
|
if security find-identity -p codesigning -v | grep -q "0 valid identities found"; then
|
||||||
|
echo "No valid macOS codesigning identity found; applying ad-hoc signature"
|
||||||
|
for app in "${apps[@]}"; do
|
||||||
|
echo "codesign (adhoc): $app"
|
||||||
|
codesign --force --deep --sign - "$app"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$app"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "macOS codesigning identity present; skipping ad-hoc signing"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Repackage Electron macOS zips (ditto)
|
- name: Repackage Electron macOS zips (ditto)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -84,9 +129,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
release_root="packages/electron-app/release"
|
release_root="packages/electron-app/release"
|
||||||
shopt -s nullglob globstar
|
# macOS GitHub runners ship /bin/bash 3.2 which doesn't support `shopt -s globstar`.
|
||||||
|
# Use find to locate built app bundles instead of ** globs.
|
||||||
apps=("$release_root"/**/CodeNomad.app)
|
apps=()
|
||||||
|
while IFS= read -r -d '' app; do
|
||||||
|
apps+=("$app")
|
||||||
|
done < <(find "$release_root" -type d -name 'CodeNomad.app' -print0)
|
||||||
if [ "${#apps[@]}" -eq 0 ]; then
|
if [ "${#apps[@]}" -eq 0 ]; then
|
||||||
echo "No CodeNomad.app found under $release_root" >&2
|
echo "No CodeNomad.app found under $release_root" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
@@ -11985,7 +11985,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
@@ -12021,7 +12021,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
@@ -12062,7 +12062,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
@@ -12070,7 +12070,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -431,7 +431,9 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
|
|
||||||
if (options.dev) {
|
if (options.dev) {
|
||||||
const devServer = process.env.VITE_DEV_SERVER_URL || process.env.ELECTRON_RENDERER_URL || "http://localhost:3000"
|
const devServer = process.env.VITE_DEV_SERVER_URL || process.env.ELECTRON_RENDERER_URL || "http://localhost:3000"
|
||||||
args.push("--ui-dev-server", devServer, "--log-level", "debug")
|
const rawLogLevel = (process.env.CLI_LOG_LEVEL ?? "info").trim()
|
||||||
|
const logLevel = rawLogLevel.length > 0 ? rawLogLevel.toLowerCase() : "info"
|
||||||
|
args.push("--ui-dev-server", devServer, "--log-level", logLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -15,7 +15,10 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/NeuralNomadsAI/CodeNomad",
|
"homepage": "https://github.com/NeuralNomadsAI/CodeNomad",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "electron-vite dev",
|
"dev": "npm run dev:info",
|
||||||
|
"dev:info": "cross-env CLI_LOG_LEVEL=info electron-vite dev",
|
||||||
|
"dev:debug": "cross-env CLI_LOG_LEVEL=debug electron-vite dev",
|
||||||
|
"dev:trace": "cross-env CLI_LOG_LEVEL=trace electron-vite dev",
|
||||||
"dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts",
|
"dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
@@ -42,6 +45,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
"app-builder-bin": "^4.2.0",
|
"app-builder-bin": "^4.2.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"electron": "39.0.0",
|
"electron": "39.0.0",
|
||||||
"electron-builder": "^24.0.0",
|
"electron-builder": "^24.0.0",
|
||||||
"electron-vite": "4.0.1",
|
"electron-vite": "4.0.1",
|
||||||
|
|||||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.11.4",
|
"version": "0.11.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const desktopQuery = useMediaQuery("(min-width: 1280px)")
|
const desktopQuery = useMediaQuery("(min-width: 1280px)")
|
||||||
|
|
||||||
const tabletQuery = useMediaQuery("(min-width: 768px)")
|
const tabletQuery = useMediaQuery("(min-width: 768px)")
|
||||||
|
const compactHeaderQuery = useMediaQuery("(max-width: 1024px)")
|
||||||
|
|
||||||
const layoutMode = createMemo<LayoutMode>(() => {
|
const layoutMode = createMemo<LayoutMode>(() => {
|
||||||
if (desktopQuery()) return "desktop"
|
if (desktopQuery()) return "desktop"
|
||||||
@@ -123,6 +124,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
||||||
|
const compactHeaderLayout = createMemo(() => isPhoneLayout() || compactHeaderQuery())
|
||||||
const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout())
|
const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout())
|
||||||
const compactPromptLayout = createMemo(() => layoutMode() !== "desktop")
|
const compactPromptLayout = createMemo(() => layoutMode() !== "desktop")
|
||||||
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||||
@@ -596,7 +598,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
||||||
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
||||||
<Show
|
<Show
|
||||||
when={!isPhoneLayout()}
|
when={!compactHeaderLayout()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="flex flex-col w-full gap-1.5">
|
<div class="flex flex-col w-full gap-1.5">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
||||||
@@ -634,8 +636,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
<span class="connection-status-shortcut-hint kbd-hint">
|
<span class="connection-status-shortcut-hint kbd-hint">
|
||||||
<Kbd shortcut="cmd+shift+p" />
|
<Kbd shortcut="cmd+shift+p" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 flex items-center justify-center min-w-0">
|
<div class="flex-1 flex items-center justify-center min-w-0">
|
||||||
<span
|
<span
|
||||||
@@ -646,7 +648,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!props.mobileFullscreenMode}>
|
<Show when={isPhoneLayout() && !props.mobileFullscreenMode}>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={props.onEnterMobileFullscreen}
|
onClick={props.onEnterMobileFullscreen}
|
||||||
@@ -670,16 +672,18 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
{rightAppBarButtonIcon()}
|
{rightAppBarButtonIcon()}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
||||||
<ContextMeter
|
<Show when={!showingInfoView()}>
|
||||||
usedTokens={tokenStats().used}
|
<ContextMeter
|
||||||
availableTokens={tokenStats().avail}
|
usedTokens={tokenStats().used}
|
||||||
formatTokens={formatTokenTotal}
|
availableTokens={tokenStats().avail}
|
||||||
usedLabel={t("instanceShell.metrics.usedLabel")}
|
formatTokens={formatTokenTotal}
|
||||||
availableLabel={t("instanceShell.metrics.availableLabel")}
|
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||||
/>
|
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type Accessor,
|
type Accessor,
|
||||||
type Component,
|
type Component,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { FileContent, FileNode, File as GitFileStatus } from "@opencode-ai/sdk/v2/client"
|
import type { FileContent, FileNode, File as GitFileStatus } from "@opencode-ai/sdk/v2/client"
|
||||||
import IconButton from "@suid/material/IconButton"
|
import IconButton from "@suid/material/IconButton"
|
||||||
import MenuOpenIcon from "@suid/icons-material/MenuOpen"
|
import MenuOpenIcon from "@suid/icons-material/MenuOpen"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { For, Show, type Accessor, type Component } from "solid-js"
|
import { For, Show, type Accessor, type Component } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import { Accordion } from "@kobalte/core"
|
import { Accordion } from "@kobalte/core"
|
||||||
import { Tooltip } from "@kobalte/core/tooltip"
|
import { Tooltip } from "@kobalte/core/tooltip"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { batch, createMemo, type Accessor } from "solid-js"
|
import { batch, createMemo, type Accessor } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { Session } from "../../../types/session"
|
import type { Session } from "../../../types/session"
|
||||||
import {
|
import {
|
||||||
activeParentSessionId,
|
activeParentSessionId,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Index, type Accessor } from "solid-js"
|
|||||||
import VirtualItem from "./virtual-item"
|
import VirtualItem from "./virtual-item"
|
||||||
import MessageBlock from "./message-block"
|
import MessageBlock from "./message-block"
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
export function getMessageAnchorId(messageId: string) {
|
export function getMessageAnchorId(messageId: string) {
|
||||||
return `message-anchor-${messageId}`
|
return `message-anchor-${messageId}`
|
||||||
@@ -23,6 +24,8 @@ interface MessageBlockListProps {
|
|||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
|
deleteHover?: Accessor<DeleteHoverState>
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
setBottomSentinel: (element: HTMLDivElement | null) => void
|
setBottomSentinel: (element: HTMLDivElement | null) => void
|
||||||
suspendMeasurements?: () => boolean
|
suspendMeasurements?: () => boolean
|
||||||
}
|
}
|
||||||
@@ -51,6 +54,8 @@ export default function MessageBlockList(props: MessageBlockListProps) {
|
|||||||
showThinking={props.showThinking}
|
showThinking={props.showThinking}
|
||||||
thinkingDefaultExpanded={props.thinkingDefaultExpanded}
|
thinkingDefaultExpanded={props.thinkingDefaultExpanded}
|
||||||
showUsageMetrics={props.showUsageMetrics}
|
showUsageMetrics={props.showUsageMetrics}
|
||||||
|
deleteHover={props.deleteHover}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
||||||
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, Trash2 } from "lucide-solid"
|
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, MessageSquareX, Trash2 } from "lucide-solid"
|
||||||
import MessageItem from "./message-item"
|
import MessageItem from "./message-item"
|
||||||
import ToolCall from "./tool-call"
|
import ToolCall from "./tool-call"
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
@@ -13,7 +13,9 @@ import { sessions, setActiveParentSession, setActiveSession } from "../stores/se
|
|||||||
import { setActiveInstanceId } from "../stores/instances"
|
import { setActiveInstanceId } from "../stores/instances"
|
||||||
import { showAlertDialog } from "../stores/alerts"
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
import { deleteMessagePart } from "../stores/session-actions"
|
import { deleteMessagePart } from "../stores/session-actions"
|
||||||
|
import { deleteMessage } from "../stores/session-actions"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
const TOOL_ICON = "🔧"
|
const TOOL_ICON = "🔧"
|
||||||
const USER_BORDER_COLOR = "var(--message-user-border)"
|
const USER_BORDER_COLOR = "var(--message-user-border)"
|
||||||
@@ -23,10 +25,10 @@ const TOOL_BORDER_COLOR = "var(--message-tool-border)"
|
|||||||
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
||||||
|
|
||||||
|
|
||||||
type ToolState = import("@opencode-ai/sdk").ToolState
|
type ToolState = import("@opencode-ai/sdk/v2").ToolState
|
||||||
type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning
|
type ToolStateRunning = import("@opencode-ai/sdk/v2").ToolStateRunning
|
||||||
type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted
|
type ToolStateCompleted = import("@opencode-ai/sdk/v2").ToolStateCompleted
|
||||||
type ToolStateError = import("@opencode-ai/sdk").ToolStateError
|
type ToolStateError = import("@opencode-ai/sdk/v2").ToolStateError
|
||||||
|
|
||||||
function isToolStateRunning(state: ToolState | undefined): state is ToolStateRunning {
|
function isToolStateRunning(state: ToolState | undefined): state is ToolStateRunning {
|
||||||
return Boolean(state && state.status === "running")
|
return Boolean(state && state.status === "running")
|
||||||
@@ -196,6 +198,8 @@ interface MessageContentItemProps {
|
|||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSupportedPartType(part: unknown): boolean {
|
function isSupportedPartType(part: unknown): boolean {
|
||||||
@@ -282,6 +286,8 @@ function MessageContentItem(props: MessageContentItemProps) {
|
|||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
isQueued={isQueued()}
|
isQueued={isQueued()}
|
||||||
showAgentMeta={showAgentMeta()}
|
showAgentMeta={showAgentMeta()}
|
||||||
|
showDeleteMessage={props.showDeleteMessage}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
@@ -298,11 +304,15 @@ interface ToolCallItemProps {
|
|||||||
messageId: string
|
messageId: string
|
||||||
partId: string
|
partId: string
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolCallItem(props: ToolCallItemProps) {
|
function ToolCallItem(props: ToolCallItemProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [deleting, setDeleting] = createSignal(false)
|
const [deleting, setDeleting] = createSignal(false)
|
||||||
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
|
const [hoverDeletePart, setHoverDeletePart] = createSignal(false)
|
||||||
|
|
||||||
const record = createMemo(() => props.store().getMessage(props.messageId))
|
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||||
@@ -370,10 +380,31 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
if (!props.showDeleteMessage) return
|
||||||
|
if (deletingMessage()) return
|
||||||
|
|
||||||
|
setDeletingMessage(true)
|
||||||
|
try {
|
||||||
|
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={toolPart()}>
|
<Show when={toolPart()}>
|
||||||
{(resolvedToolPart) => (
|
{(resolvedToolPart) => (
|
||||||
<>
|
<div class="delete-hover-scope" data-delete-part-hover={hoverDeletePart() ? "true" : undefined}>
|
||||||
<div class="tool-call-header-label">
|
<div class="tool-call-header-label">
|
||||||
<div class="tool-call-header-meta">
|
<div class="tool-call-header-meta">
|
||||||
<span class="tool-call-icon">{TOOL_ICON}</span>
|
<span class="tool-call-icon">{TOOL_ICON}</span>
|
||||||
@@ -381,7 +412,7 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Show when={taskSessionId()}>
|
<Show when={taskSessionId()}>
|
||||||
<button
|
<button
|
||||||
class="tool-call-header-button"
|
class="tool-call-header-button"
|
||||||
@@ -395,18 +426,41 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="tool-call-header-button"
|
class="tool-call-header-button"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={deleteDisabled()}
|
disabled={deleteDisabled()}
|
||||||
onClick={handleDeleteToolPart}
|
onClick={handleDeleteToolPart}
|
||||||
title={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
onMouseEnter={() => {
|
||||||
aria-label={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
setHoverDeletePart(true)
|
||||||
>
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "tool" })
|
||||||
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
}}
|
||||||
</button>
|
onMouseLeave={() => {
|
||||||
|
setHoverDeletePart(false)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
|
title={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
||||||
|
aria-label={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
||||||
|
>
|
||||||
|
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<button
|
||||||
|
class="tool-call-header-button"
|
||||||
|
type="button"
|
||||||
|
disabled={deletingMessage()}
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<ToolCall
|
<ToolCall
|
||||||
toolCall={resolvedToolPart()}
|
toolCall={resolvedToolPart()}
|
||||||
@@ -418,7 +472,7 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
@@ -470,6 +524,8 @@ interface MessageBlockProps {
|
|||||||
showThinking: () => boolean
|
showThinking: () => boolean
|
||||||
thinkingDefaultExpanded: () => boolean
|
thinkingDefaultExpanded: () => boolean
|
||||||
showUsageMetrics: () => boolean
|
showUsageMetrics: () => boolean
|
||||||
|
deleteHover?: () => DeleteHoverState
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
@@ -481,6 +537,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||||
const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId)
|
const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId)
|
||||||
|
|
||||||
|
const isDeleteMessageHovered = () => {
|
||||||
|
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
|
||||||
|
return hover.kind === "message" && hover.messageId === props.messageId
|
||||||
|
}
|
||||||
|
|
||||||
const block = createMemo<MessageDisplayBlock | null>(() => {
|
const block = createMemo<MessageDisplayBlock | null>(() => {
|
||||||
const current = record()
|
const current = record()
|
||||||
if (!current) return null
|
if (!current) return null
|
||||||
@@ -668,9 +729,13 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
return (
|
return (
|
||||||
<Show when={block()}>
|
<Show when={block()}>
|
||||||
{(resolvedBlock) => (
|
{(resolvedBlock) => (
|
||||||
<div class="message-stream-block" data-message-id={resolvedBlock().record.id}>
|
<div
|
||||||
|
class="message-stream-block"
|
||||||
|
data-message-id={resolvedBlock().record.id}
|
||||||
|
data-delete-message-hover={isDeleteMessageHovered() ? "true" : undefined}
|
||||||
|
>
|
||||||
<For each={resolvedBlock().items}>
|
<For each={resolvedBlock().items}>
|
||||||
{(item) => (
|
{(item, index) => (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={item.type === "content"}>
|
<Match when={item.type === "content"}>
|
||||||
<MessageContentItem
|
<MessageContentItem
|
||||||
@@ -681,6 +746,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
startPartId={(item as ContentDisplayItem).startPartId}
|
startPartId={(item as ContentDisplayItem).startPartId}
|
||||||
messageIndex={props.messageIndex}
|
messageIndex={props.messageIndex}
|
||||||
lastAssistantIndex={props.lastAssistantIndex}
|
lastAssistantIndex={props.lastAssistantIndex}
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
@@ -697,6 +764,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
store={props.store}
|
store={props.store}
|
||||||
messageId={toolItem.messageId}
|
messageId={toolItem.messageId}
|
||||||
partId={toolItem.partId}
|
partId={toolItem.partId}
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -709,6 +778,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
part={(item as StepDisplayItem).part}
|
part={(item as StepDisplayItem).part}
|
||||||
messageInfo={(item as StepDisplayItem).messageInfo}
|
messageInfo={(item as StepDisplayItem).messageInfo}
|
||||||
showAgentMeta
|
showAgentMeta
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
instanceId={props.instanceId}
|
||||||
|
sessionId={props.sessionId}
|
||||||
|
messageId={props.messageId}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "step-finish"}>
|
<Match when={item.type === "step-finish"}>
|
||||||
@@ -718,6 +792,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageInfo={(item as StepDisplayItem).messageInfo}
|
messageInfo={(item as StepDisplayItem).messageInfo}
|
||||||
showUsage={props.showUsageMetrics()}
|
showUsage={props.showUsageMetrics()}
|
||||||
borderColor={(item as StepDisplayItem).accentColor}
|
borderColor={(item as StepDisplayItem).accentColor}
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
instanceId={props.instanceId}
|
||||||
|
sessionId={props.sessionId}
|
||||||
|
messageId={props.messageId}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "compaction"}>
|
<Match when={item.type === "compaction"}>
|
||||||
@@ -729,6 +808,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
messageId={(item as CompactionDisplayItem).messageId}
|
messageId={(item as CompactionDisplayItem).messageId}
|
||||||
partId={(item as CompactionDisplayItem).partId}
|
partId={(item as CompactionDisplayItem).partId}
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "reasoning"}>
|
<Match when={item.type === "reasoning"}>
|
||||||
@@ -741,6 +822,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
partId={(item as ReasoningDisplayItem).partId}
|
partId={(item as ReasoningDisplayItem).partId}
|
||||||
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
|
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
|
||||||
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
|
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -759,6 +842,11 @@ interface StepCardProps {
|
|||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
showUsage?: boolean
|
showUsage?: boolean
|
||||||
borderColor?: string
|
borderColor?: string
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
instanceId?: string
|
||||||
|
sessionId?: string
|
||||||
|
messageId?: string
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CompactionCardProps {
|
interface CompactionCardProps {
|
||||||
@@ -769,11 +857,15 @@ interface CompactionCardProps {
|
|||||||
sessionId: string
|
sessionId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
partId: string
|
partId: string
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompactionCard(props: CompactionCardProps) {
|
function CompactionCard(props: CompactionCardProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [deleting, setDeleting] = createSignal(false)
|
const [deleting, setDeleting] = createSignal(false)
|
||||||
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
|
const [hoverDeletePart, setHoverDeletePart] = createSignal(false)
|
||||||
const isAuto = () => Boolean((props.part as any)?.auto)
|
const isAuto = () => Boolean((props.part as any)?.auto)
|
||||||
const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel"))
|
const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel"))
|
||||||
const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR)
|
const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR)
|
||||||
@@ -801,22 +893,70 @@ function CompactionCard(props: CompactionCardProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||||
|
|
||||||
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!props.showDeleteMessage) return
|
||||||
|
if (!canDeleteMessage()) return
|
||||||
|
setDeletingMessage(true)
|
||||||
|
try {
|
||||||
|
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`${containerClass()} relative`}
|
class={`delete-hover-scope ${containerClass()} relative`}
|
||||||
|
data-delete-part-hover={hoverDeletePart() ? "true" : undefined}
|
||||||
style={{ "border-left": `4px solid ${borderColor()}` }}
|
style={{ "border-left": `4px solid ${borderColor()}` }}
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={t("messageBlock.compaction.ariaLabel")}
|
aria-label={t("messageBlock.compaction.ariaLabel")}
|
||||||
>
|
>
|
||||||
<button
|
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||||
type="button"
|
<Show when={props.showDeleteMessage}>
|
||||||
class="tool-call-header-button absolute right-2 top-1/2 -translate-y-1/2"
|
<button
|
||||||
disabled={!canDelete()}
|
type="button"
|
||||||
onClick={handleDelete}
|
class="tool-call-header-button"
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
disabled={!canDeleteMessage()}
|
||||||
>
|
onClick={handleDeleteMessage}
|
||||||
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
</button>
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-header-button"
|
||||||
|
disabled={!canDelete()}
|
||||||
|
onClick={handleDelete}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setHoverDeletePart(true)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "compaction" })
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoverDeletePart(false)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
|
>
|
||||||
|
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="message-compaction-row">
|
<div class="message-compaction-row">
|
||||||
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
|
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
|
||||||
@@ -828,6 +968,7 @@ function CompactionCard(props: CompactionCardProps) {
|
|||||||
|
|
||||||
function StepCard(props: StepCardProps) {
|
function StepCard(props: StepCardProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
const timestamp = () => {
|
const timestamp = () => {
|
||||||
const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now()
|
const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now()
|
||||||
const date = new Date(value)
|
const date = new Date(value)
|
||||||
@@ -872,6 +1013,27 @@ function StepCard(props: StepCardProps) {
|
|||||||
|
|
||||||
const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
|
const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
|
||||||
|
|
||||||
|
const canDeleteMessage = () =>
|
||||||
|
Boolean(props.showDeleteMessage && props.instanceId && props.sessionId && props.messageId) && !deletingMessage()
|
||||||
|
|
||||||
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!canDeleteMessage()) return
|
||||||
|
setDeletingMessage(true)
|
||||||
|
try {
|
||||||
|
await deleteMessage(props.instanceId!, props.sessionId!, props.messageId!)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
|
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
|
||||||
const entries = [
|
const entries = [
|
||||||
@@ -902,7 +1064,22 @@ function StepCard(props: StepCardProps) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div class={`message-step-card message-step-finish message-step-finish-flush`} style={finishStyle()}>
|
<div class={`message-step-card message-step-finish message-step-finish-flush relative`} style={finishStyle()}>
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-action-button absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
|
disabled={!canDeleteMessage()}
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId! })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{renderUsageChips(usage)}
|
{renderUsageChips(usage)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -942,12 +1119,16 @@ interface ReasoningCardProps {
|
|||||||
partId: string
|
partId: string
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
defaultExpanded?: boolean
|
defaultExpanded?: boolean
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReasoningCard(props: ReasoningCardProps) {
|
function ReasoningCard(props: ReasoningCardProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
|
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
|
||||||
const [deleting, setDeleting] = createSignal(false)
|
const [deleting, setDeleting] = createSignal(false)
|
||||||
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
|
const [hoverDeletePart, setHoverDeletePart] = createSignal(false)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setExpanded(Boolean(props.defaultExpanded))
|
setExpanded(Boolean(props.defaultExpanded))
|
||||||
@@ -1035,8 +1216,29 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||||
|
|
||||||
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!props.showDeleteMessage) return
|
||||||
|
if (!canDeleteMessage()) return
|
||||||
|
setDeletingMessage(true)
|
||||||
|
try {
|
||||||
|
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="message-reasoning-card">
|
<div class="delete-hover-scope message-reasoning-card" data-delete-part-hover={hoverDeletePart() ? "true" : undefined}>
|
||||||
<div class="message-reasoning-header">
|
<div class="message-reasoning-header">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1087,6 +1289,14 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={!canDelete()}
|
disabled={!canDelete()}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setHoverDeletePart(true)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "reasoning" })
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoverDeletePart(false)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
aria-label={t("messagePart.actions.deleteTitle")}
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
>
|
>
|
||||||
@@ -1094,6 +1304,21 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-action-button"
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
disabled={!canDeleteMessage()}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<span class="message-reasoning-time">{timestamp()}</span>
|
<span class="message-reasoning-time">{timestamp()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { For, Show, createSignal } from "solid-js"
|
import { For, Show, createSignal } from "solid-js"
|
||||||
import { Copy, ExternalLink, Split, Trash2, Undo } from "lucide-solid"
|
import { Copy, MessageSquareX, Split, Trash2, Undo } from "lucide-solid"
|
||||||
import type { MessageInfo, ClientPart } from "../types/message"
|
import type { MessageInfo, ClientPart, SDKAssistantMessageV2 } from "../types/message"
|
||||||
import { partHasRenderableText } from "../types/message"
|
import { partHasRenderableText } from "../types/message"
|
||||||
import type { MessageRecord } from "../stores/message-v2/types"
|
import type { MessageRecord } from "../stores/message-v2/types"
|
||||||
import MessagePart from "./message-part"
|
import MessagePart from "./message-part"
|
||||||
import { copyToClipboard } from "../lib/clipboard"
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
import { showAlertDialog } from "../stores/alerts"
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
import { deleteMessagePart } from "../stores/session-actions"
|
import { deleteMessage, deleteMessagePart } from "../stores/session-actions"
|
||||||
import { isTauriHost } from "../lib/runtime-env"
|
import { isTauriHost } from "../lib/runtime-env"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
record: MessageRecord
|
record: MessageRecord
|
||||||
@@ -21,12 +22,16 @@ interface MessageItemProps {
|
|||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MessageItem(props: MessageItemProps) {
|
export default function MessageItem(props: MessageItemProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [copied, setCopied] = createSignal(false)
|
const [copied, setCopied] = createSignal(false)
|
||||||
const [deletingParts, setDeletingParts] = createSignal<Set<string>>(new Set())
|
const [deletingParts, setDeletingParts] = createSignal<Set<string>>(new Set())
|
||||||
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
|
const [hoveredDeletePartId, setHoveredDeletePartId] = createSignal<string | null>(null)
|
||||||
|
|
||||||
const isUser = () => props.record.role === "user"
|
const isUser = () => props.record.role === "user"
|
||||||
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
|
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
|
||||||
@@ -234,6 +239,22 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteMessage = async () => {
|
||||||
|
if (deletingMessage()) return
|
||||||
|
setDeletingMessage(true)
|
||||||
|
try {
|
||||||
|
await deleteMessage(props.instanceId, props.sessionId, props.record.id)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isUser() && !hasContent() && !isGenerating()) {
|
if (!isUser() && !hasContent() && !isGenerating()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -258,8 +279,16 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
if (!info || info.role !== "assistant") return ""
|
if (!info || info.role !== "assistant") return ""
|
||||||
const modelID = info.modelID || ""
|
const modelID = info.modelID || ""
|
||||||
const providerID = info.providerID || ""
|
const providerID = info.providerID || ""
|
||||||
if (modelID && providerID) return `${providerID}/${modelID}`
|
|
||||||
return modelID
|
const base = modelID && providerID ? `${providerID}/${modelID}` : modelID
|
||||||
|
if (!base) return ""
|
||||||
|
|
||||||
|
const variant = (info as SDKAssistantMessageV2).variant
|
||||||
|
if (typeof variant === "string" && variant.trim().length > 0) {
|
||||||
|
return `${base} (${variant.trim()})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentMeta = () => {
|
const agentMeta = () => {
|
||||||
@@ -290,16 +319,15 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
<div class="message-item-actions">
|
<div class="message-item-actions">
|
||||||
<Show when={isUser()}>
|
<Show when={isUser()}>
|
||||||
<div class="message-action-group">
|
<div class="message-action-group">
|
||||||
<Show when={props.onRevert}>
|
<button
|
||||||
<button
|
class="message-action-button"
|
||||||
class="message-action-button"
|
onClick={handleCopy}
|
||||||
onClick={handleRevert}
|
title={copyLabel()}
|
||||||
title={t("messageItem.actions.revert")}
|
aria-label={copyLabel()}
|
||||||
aria-label={t("messageItem.actions.revert")}
|
>
|
||||||
>
|
<Copy class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
<Undo class="w-3.5 h-3.5" aria-hidden="true" />
|
</button>
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<Show when={props.onFork}>
|
<Show when={props.onFork}>
|
||||||
<button
|
<button
|
||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
@@ -310,14 +338,31 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
<Split class="w-3.5 h-3.5" aria-hidden="true" />
|
<Split class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
<button
|
|
||||||
class="message-action-button"
|
<Show when={props.onRevert}>
|
||||||
onClick={handleCopy}
|
<button
|
||||||
title={copyLabel()}
|
class="message-action-button"
|
||||||
aria-label={copyLabel()}
|
onClick={handleRevert}
|
||||||
>
|
title={t("messageItem.actions.revertTitle")}
|
||||||
<Copy class="w-3.5 h-3.5" aria-hidden="true" />
|
aria-label={t("messageItem.actions.revertTitle")}
|
||||||
</button>
|
>
|
||||||
|
<Undo class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<button
|
||||||
|
class="message-action-button"
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
disabled={deletingMessage()}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!isUser()}>
|
<Show when={!isUser()}>
|
||||||
@@ -337,6 +382,14 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
onClick={() => void handleDeletePart(partId())}
|
onClick={() => void handleDeletePart(partId())}
|
||||||
disabled={isDeletingPart(partId())}
|
disabled={isDeletingPart(partId())}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setHoveredDeletePartId(partId())
|
||||||
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.record.id, partId: partId(), partType: "text" })
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoveredDeletePartId(null)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
title={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
title={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||||
aria-label={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
aria-label={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||||
>
|
>
|
||||||
@@ -344,6 +397,20 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<button
|
||||||
|
class="message-action-button"
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
disabled={deletingMessage()}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
|
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
|
||||||
@@ -378,16 +445,27 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<For each={messageParts()}>
|
<For each={messageParts()}>
|
||||||
{(part) => (
|
{(part) => {
|
||||||
<MessagePart
|
const partId = typeof (part as any)?.id === "string" ? ((part as any).id as string) : ""
|
||||||
part={part}
|
const isHoveredDeleteTarget = () => Boolean(partId) && hoveredDeletePartId() === partId
|
||||||
messageType={props.record.role}
|
|
||||||
instanceId={props.instanceId}
|
return (
|
||||||
sessionId={props.sessionId}
|
<div
|
||||||
primaryUserTextPartId={primaryUserTextPartId()}
|
class="delete-hover-scope message-part-shell"
|
||||||
onRendered={props.onContentRendered}
|
data-part-id={partId}
|
||||||
/>
|
data-delete-part-hover={isHoveredDeleteTarget() ? "true" : undefined}
|
||||||
)}
|
>
|
||||||
|
<MessagePart
|
||||||
|
part={part}
|
||||||
|
messageType={props.record.role}
|
||||||
|
instanceId={props.instanceId}
|
||||||
|
sessionId={props.sessionId}
|
||||||
|
primaryUserTextPartId={primaryUserTextPartId()}
|
||||||
|
onRendered={props.onContentRendered}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<Show when={fileAttachments().length > 0}>
|
<Show when={fileAttachments().length > 0}>
|
||||||
@@ -396,8 +474,13 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
{(attachment) => {
|
{(attachment) => {
|
||||||
const name = getAttachmentName(attachment)
|
const name = getAttachmentName(attachment)
|
||||||
const isImage = isImageAttachment(attachment)
|
const isImage = isImageAttachment(attachment)
|
||||||
|
const isHoveredDeleteTarget = () => hoveredDeletePartId() === attachment.id
|
||||||
return (
|
return (
|
||||||
<div class={`attachment-chip ${isImage ? "attachment-chip-image" : ""}`} title={name}>
|
<div
|
||||||
|
class={`delete-hover-scope attachment-chip ${isImage ? "attachment-chip-image" : ""}`}
|
||||||
|
data-delete-part-hover={isHoveredDeleteTarget() ? "true" : undefined}
|
||||||
|
title={name}
|
||||||
|
>
|
||||||
<Show when={isImage} fallback={
|
<Show when={isImage} fallback={
|
||||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path
|
<path
|
||||||
@@ -431,6 +514,16 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
onClick={() => void handleDeletePart(attachment.id)}
|
onClick={() => void handleDeletePart(attachment.id)}
|
||||||
class="attachment-remove"
|
class="attachment-remove"
|
||||||
disabled={isDeletingPart(attachment.id)}
|
disabled={isDeletingPart(attachment.id)}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (attachment.id) {
|
||||||
|
setHoveredDeletePartId(attachment.id)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.record.id, partId: attachment.id, partType: "file" })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoveredDeletePartId(null)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
aria-label={t("messagePart.actions.deleteTitle")}
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useI18n } from "../lib/i18n"
|
|||||||
import { copyToClipboard } from "../lib/clipboard"
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
import { showToastNotification } from "../lib/notifications"
|
import { showToastNotification } from "../lib/notifications"
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
const SCROLL_SCOPE = "session"
|
const SCROLL_SCOPE = "session"
|
||||||
const SCROLL_SENTINEL_MARGIN_PX = 48
|
const SCROLL_SENTINEL_MARGIN_PX = 48
|
||||||
@@ -145,6 +146,8 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [activeMessageId, setActiveMessageId] = createSignal<string | null>(null)
|
const [activeMessageId, setActiveMessageId] = createSignal<string | null>(null)
|
||||||
|
|
||||||
|
const [deleteHover, setDeleteHover] = createSignal<DeleteHoverState>({ kind: "none" })
|
||||||
|
|
||||||
const changeToken = createMemo(() => String(sessionRevision()))
|
const changeToken = createMemo(() => String(sessionRevision()))
|
||||||
const isActive = createMemo(() => props.isActive !== false)
|
const isActive = createMemo(() => props.isActive !== false)
|
||||||
@@ -899,6 +902,8 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={handleContentRendered}
|
onContentRendered={handleContentRendered}
|
||||||
|
deleteHover={deleteHover}
|
||||||
|
onDeleteHoverChange={setDeleteHover}
|
||||||
setBottomSentinel={setBottomSentinel}
|
setBottomSentinel={setBottomSentinel}
|
||||||
suspendMeasurements={() => !isActive()}
|
suspendMeasurements={() => !isActive()}
|
||||||
/>
|
/>
|
||||||
@@ -957,6 +962,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
showToolSegments={showTimelineToolsPreference()}
|
showToolSegments={showTimelineToolsPreference()}
|
||||||
|
deleteHover={deleteHover}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show, createEffect, createMemo, createSignal, onCleanup, type Component } from "solid-js"
|
import { For, Show, createEffect, createMemo, createSignal, onCleanup, on, untrack, type Component } from "solid-js"
|
||||||
import MessagePreview from "./message-preview"
|
import MessagePreview from "./message-preview"
|
||||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||||
import type { ClientPart } from "../types/message"
|
import type { ClientPart } from "../types/message"
|
||||||
@@ -7,6 +7,7 @@ import { buildRecordDisplayData } from "../stores/message-v2/record-display-cach
|
|||||||
import { getToolIcon } from "./tool-call/utils"
|
import { getToolIcon } from "./tool-call/utils"
|
||||||
import { User as UserIcon, Bot as BotIcon, FoldVertical, ShieldAlert } from "lucide-solid"
|
import { User as UserIcon, Bot as BotIcon, FoldVertical, ShieldAlert } from "lucide-solid"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction"
|
export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction"
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ export interface TimelineSegment {
|
|||||||
shortLabel?: string
|
shortLabel?: string
|
||||||
variant?: "auto" | "manual"
|
variant?: "auto" | "manual"
|
||||||
toolPartIds?: string[]
|
toolPartIds?: string[]
|
||||||
|
partIds?: string[]
|
||||||
|
partId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageTimelineProps {
|
interface MessageTimelineProps {
|
||||||
@@ -28,6 +31,7 @@ interface MessageTimelineProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
showToolSegments?: boolean
|
showToolSegments?: boolean
|
||||||
|
deleteHover?: () => DeleteHoverState
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TOOLTIP_LENGTH = 220
|
const MAX_TOOLTIP_LENGTH = 220
|
||||||
@@ -42,6 +46,7 @@ interface PendingSegment {
|
|||||||
toolTypeLabels: string[]
|
toolTypeLabels: string[]
|
||||||
toolIcons: string[]
|
toolIcons: string[]
|
||||||
toolPartIds: string[]
|
toolPartIds: string[]
|
||||||
|
partIds: string[]
|
||||||
hasPrimaryText: boolean
|
hasPrimaryText: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +196,7 @@ export function buildTimelineSegments(
|
|||||||
tooltip,
|
tooltip,
|
||||||
shortLabel,
|
shortLabel,
|
||||||
toolPartIds: isToolSegment ? pending.toolPartIds : undefined,
|
toolPartIds: isToolSegment ? pending.toolPartIds : undefined,
|
||||||
|
partIds: !isToolSegment ? pending.partIds : undefined,
|
||||||
})
|
})
|
||||||
segmentIndex += 1
|
segmentIndex += 1
|
||||||
pending = null
|
pending = null
|
||||||
@@ -199,7 +205,17 @@ export function buildTimelineSegments(
|
|||||||
const ensureSegment = (type: TimelineSegmentType): PendingSegment => {
|
const ensureSegment = (type: TimelineSegmentType): PendingSegment => {
|
||||||
if (!pending || pending.type !== type) {
|
if (!pending || pending.type !== type) {
|
||||||
flushPending()
|
flushPending()
|
||||||
pending = { type, texts: [], reasoningTexts: [], toolTitles: [], toolTypeLabels: [], toolIcons: [], toolPartIds: [], hasPrimaryText: type !== "assistant" }
|
pending = {
|
||||||
|
type,
|
||||||
|
texts: [],
|
||||||
|
reasoningTexts: [],
|
||||||
|
toolTitles: [],
|
||||||
|
toolTypeLabels: [],
|
||||||
|
toolIcons: [],
|
||||||
|
toolPartIds: [],
|
||||||
|
partIds: [],
|
||||||
|
hasPrimaryText: type !== "assistant",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return pending!
|
return pending!
|
||||||
}
|
}
|
||||||
@@ -228,6 +244,9 @@ export function buildTimelineSegments(
|
|||||||
const target = ensureSegment(defaultContentType)
|
const target = ensureSegment(defaultContentType)
|
||||||
if (target) {
|
if (target) {
|
||||||
target.reasoningTexts.push(text)
|
target.reasoningTexts.push(text)
|
||||||
|
if (typeof (part as any).id === "string" && (part as any).id.length > 0) {
|
||||||
|
target.partIds.push((part as any).id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -235,6 +254,7 @@ export function buildTimelineSegments(
|
|||||||
if (part.type === "compaction") {
|
if (part.type === "compaction") {
|
||||||
flushPending()
|
flushPending()
|
||||||
const isAuto = Boolean((part as any)?.auto)
|
const isAuto = Boolean((part as any)?.auto)
|
||||||
|
const partId = typeof (part as any)?.id === "string" ? ((part as any).id as string) : ""
|
||||||
result.push({
|
result.push({
|
||||||
id: `${record.id}:${segmentIndex}`,
|
id: `${record.id}:${segmentIndex}`,
|
||||||
messageId: record.id,
|
messageId: record.id,
|
||||||
@@ -242,6 +262,7 @@ export function buildTimelineSegments(
|
|||||||
label: segmentLabel("compaction"),
|
label: segmentLabel("compaction"),
|
||||||
tooltip: isAuto ? t("messageTimeline.tooltip.compaction.auto") : t("messageTimeline.tooltip.compaction.manual"),
|
tooltip: isAuto ? t("messageTimeline.tooltip.compaction.auto") : t("messageTimeline.tooltip.compaction.manual"),
|
||||||
variant: isAuto ? "auto" : "manual",
|
variant: isAuto ? "auto" : "manual",
|
||||||
|
partId,
|
||||||
})
|
})
|
||||||
segmentIndex += 1
|
segmentIndex += 1
|
||||||
continue
|
continue
|
||||||
@@ -257,6 +278,9 @@ export function buildTimelineSegments(
|
|||||||
if (target) {
|
if (target) {
|
||||||
target.texts.push(text)
|
target.texts.push(text)
|
||||||
target.hasPrimaryText = true
|
target.hasPrimaryText = true
|
||||||
|
if (typeof (part as any).id === "string" && (part as any).id.length > 0) {
|
||||||
|
target.partIds.push((part as any).id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +302,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
let hoverTimer: number | null = null
|
let hoverTimer: number | null = null
|
||||||
let closeTimer: number | null = null
|
let closeTimer: number | null = null
|
||||||
const showTools = () => props.showToolSegments ?? true
|
const showTools = () => props.showToolSegments ?? true
|
||||||
|
const deleteHover = () => props.deleteHover?.() ?? { kind: "none" as const }
|
||||||
|
|
||||||
const registerButtonRef = (segmentId: string, element: HTMLButtonElement | null) => {
|
const registerButtonRef = (segmentId: string, element: HTMLButtonElement | null) => {
|
||||||
if (element) {
|
if (element) {
|
||||||
@@ -350,11 +375,9 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
clearCloseTimer()
|
clearCloseTimer()
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(on(() => props.activeMessageId, (activeId) => {
|
||||||
const activeId = props.activeMessageId
|
|
||||||
|
|
||||||
if (!activeId) return
|
if (!activeId) return
|
||||||
const targetSegment = props.segments.find((segment) => segment.messageId === activeId)
|
const targetSegment = untrack(() => props.segments).find((segment) => segment.messageId === activeId)
|
||||||
if (!targetSegment) return
|
if (!targetSegment) return
|
||||||
const element = buttonRefs.get(targetSegment.id)
|
const element = buttonRefs.get(targetSegment.id)
|
||||||
if (!element) return
|
if (!element) return
|
||||||
@@ -366,7 +389,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
window.clearTimeout(timer)
|
window.clearTimeout(timer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const element = tooltipElement()
|
const element = tooltipElement()
|
||||||
@@ -428,16 +451,34 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={(el) => registerButtonRef(segment.id, el)}
|
ref={(el) => registerButtonRef(segment.id, el)}
|
||||||
type="button"
|
type="button"
|
||||||
data-variant={segment.variant}
|
data-variant={segment.variant}
|
||||||
class={`message-timeline-segment message-timeline-${segment.type} ${hasActivePermission() ? "message-timeline-segment-permission" : ""} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
|
class={`message-timeline-segment message-timeline-${segment.type} ${hasActivePermission() ? "message-timeline-segment-permission" : ""} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
|
||||||
|
|
||||||
aria-current={isActive() ? "true" : undefined}
|
data-delete-hover={(() => {
|
||||||
aria-hidden={isHidden() ? "true" : undefined}
|
const hover = deleteHover() as DeleteHoverState
|
||||||
onClick={() => props.onSegmentClick?.(segment)}
|
if (hover.kind === "message") {
|
||||||
onMouseEnter={(event) => handleMouseEnter(segment, event)}
|
return hover.messageId === segment.messageId ? "true" : undefined
|
||||||
|
}
|
||||||
|
if (hover.kind === "part") {
|
||||||
|
if (hover.messageId !== segment.messageId) return undefined
|
||||||
|
if (segment.type === "tool") {
|
||||||
|
return segment.toolPartIds?.includes(hover.partId) ? "true" : undefined
|
||||||
|
}
|
||||||
|
if (segment.type === "compaction") {
|
||||||
|
return segment.partId === hover.partId ? "true" : undefined
|
||||||
|
}
|
||||||
|
return segment.partIds?.includes(hover.partId) ? "true" : undefined
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})()}
|
||||||
|
|
||||||
|
aria-current={isActive() ? "true" : undefined}
|
||||||
|
aria-hidden={isHidden() ? "true" : undefined}
|
||||||
|
onClick={() => props.onSegmentClick?.(segment)}
|
||||||
|
onMouseEnter={(event) => handleMouseEnter(segment, event)}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<span class="message-timeline-label message-timeline-label-full">{segment.label}</span>
|
<span class="message-timeline-label message-timeline-label-full">{segment.label}</span>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import { getLogger } from "../lib/logger"
|
|||||||
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
type ToolState = import("@opencode-ai/sdk").ToolState
|
type ToolState = import("@opencode-ai/sdk/v2").ToolState
|
||||||
|
|
||||||
const TOOL_CALL_CACHE_SCOPE = "tool-call"
|
const TOOL_CALL_CACHE_SCOPE = "tool-call"
|
||||||
const TOOL_SCROLL_SENTINEL_MARGIN_PX = 48
|
const TOOL_SCROLL_SENTINEL_MARGIN_PX = 48
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import { getRelativePath, isToolStateCompleted, isToolStateError, isToolStateRunning } from "./utils"
|
import { getRelativePath, isToolStateCompleted, isToolStateError, isToolStateRunning } from "./utils"
|
||||||
import { tGlobal } from "../../lib/i18n"
|
import { tGlobal } from "../../lib/i18n"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Accessor, JSXElement } from "solid-js"
|
import type { Accessor, JSXElement } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { TextPart } from "../../types/message"
|
import type { TextPart } from "../../types/message"
|
||||||
import { Markdown } from "../markdown"
|
import { Markdown } from "../markdown"
|
||||||
import type { MarkdownRenderOptions, ToolScrollHelpers } from "./types"
|
import type { MarkdownRenderOptions, ToolScrollHelpers } from "./types"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createMemo, Show, For, createEffect, type Accessor } from "solid-js"
|
import { createMemo, Show, For, createEffect, type Accessor } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { useI18n } from "../../lib/i18n"
|
import { useI18n } from "../../lib/i18n"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { For, Show, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
import { For, Show, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { ToolRenderer } from "../types"
|
import type { ToolRenderer } from "../types"
|
||||||
import { ensureMarkdownContent, getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils"
|
import { ensureMarkdownContent, getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils"
|
||||||
import { resolveTitleForTool } from "../tool-title"
|
import { resolveTitleForTool } from "../tool-title"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { For, Show } from "solid-js"
|
import { For, Show } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { ToolRenderer } from "../types"
|
import type { ToolRenderer } from "../types"
|
||||||
import { readToolStatePayload } from "../utils"
|
import { readToolStatePayload } from "../utils"
|
||||||
import { useI18n, tGlobal } from "../../../lib/i18n"
|
import { useI18n, tGlobal } from "../../../lib/i18n"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { ToolRendererContext, ToolRenderer, ToolCallPart } from "./types"
|
import type { ToolRendererContext, ToolRenderer, ToolCallPart } from "./types"
|
||||||
import { getDefaultToolAction, getToolName, isToolStateCompleted, isToolStateRunning } from "./utils"
|
import { getDefaultToolAction, getToolName, isToolStateCompleted, isToolStateRunning } from "./utils"
|
||||||
import { enMessages } from "../../lib/i18n/messages/en"
|
import { enMessages } from "../../lib/i18n/messages/en"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Accessor, JSXElement } from "solid-js"
|
import type { Accessor, JSXElement } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { ClientPart } from "../../types/message"
|
import type { ClientPart } from "../../types/message"
|
||||||
|
|
||||||
export type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
export type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { isRenderableDiffText } from "../../lib/diff-utils"
|
import { isRenderableDiffText } from "../../lib/diff-utils"
|
||||||
import { getLanguageFromPath } from "../../lib/markdown"
|
import { getLanguageFromPath } from "../../lib/markdown"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { DiffPayload } from "./types"
|
import type { DiffPayload } from "./types"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
import { tGlobal } from "../../lib/i18n"
|
import { tGlobal } from "../../lib/i18n"
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
|
|
||||||
export type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning
|
export type ToolStateRunning = import("@opencode-ai/sdk/v2").ToolStateRunning
|
||||||
export type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted
|
export type ToolStateCompleted = import("@opencode-ai/sdk/v2").ToolStateCompleted
|
||||||
export type ToolStateError = import("@opencode-ai/sdk").ToolStateError
|
export type ToolStateError = import("@opencode-ai/sdk/v2").ToolStateError
|
||||||
|
|
||||||
export const diffCapableTools = new Set(["edit", "patch"])
|
export const diffCapableTools = new Set(["edit", "patch"])
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Go to Session",
|
"messageBlock.tool.goToSession.label": "Go to Session",
|
||||||
"messageBlock.tool.goToSession.title": "Go to session",
|
"messageBlock.tool.goToSession.title": "Go to session",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "Session not available yet",
|
"messageBlock.tool.goToSession.unavailableTitle": "Session not available yet",
|
||||||
"messageBlock.tool.deletePart.label": "Delete",
|
"messageBlock.tool.deletePart.label": "Delete Part",
|
||||||
"messageBlock.tool.deletePart.deleting": "Deleting...",
|
"messageBlock.tool.deletePart.deleting": "Deleting...",
|
||||||
"messageBlock.tool.deletePart.title": "Delete this tool call output",
|
"messageBlock.tool.deletePart.title": "Delete this tool call output",
|
||||||
"messageBlock.tool.deletePart.failed.title": "Delete failed",
|
"messageBlock.tool.deletePart.failed.title": "Delete failed",
|
||||||
@@ -71,17 +71,21 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "You",
|
"messageItem.speaker.you": "You",
|
||||||
"messageItem.speaker.assistant": "Assistant",
|
"messageItem.speaker.assistant": "Assistant",
|
||||||
"messageItem.actions.revert": "Revert",
|
"messageItem.actions.revert": "Revert",
|
||||||
"messageItem.actions.revertTitle": "Revert to this message",
|
"messageItem.actions.revertTitle": "Undo changes up to here",
|
||||||
"messageItem.actions.fork": "Fork",
|
"messageItem.actions.fork": "Fork",
|
||||||
"messageItem.actions.forkTitle": "Fork from this message",
|
"messageItem.actions.forkTitle": "Fork from this message",
|
||||||
"messageItem.actions.copy": "Copy",
|
"messageItem.actions.copy": "Copy",
|
||||||
"messageItem.actions.copyTitle": "Copy message",
|
"messageItem.actions.copyTitle": "Copy message",
|
||||||
"messageItem.actions.copied": "Copied!",
|
"messageItem.actions.copied": "Copied!",
|
||||||
|
"messageItem.actions.deleteMessage": "Delete message",
|
||||||
|
"messageItem.actions.deletingMessage": "Deleting...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "Delete failed",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "Failed to delete message",
|
||||||
"messageItem.status.queued": "QUEUED",
|
"messageItem.status.queued": "QUEUED",
|
||||||
"messageItem.status.generating": "Generating...",
|
"messageItem.status.generating": "Generating...",
|
||||||
"messageItem.status.sending": "Sending...",
|
"messageItem.status.sending": "Sending...",
|
||||||
"messageItem.status.failedToSend": "Message failed to send",
|
"messageItem.status.failedToSend": "Message failed to send",
|
||||||
"messagePart.actions.delete": "Delete",
|
"messagePart.actions.delete": "Delete Part",
|
||||||
"messagePart.actions.deleting": "Deleting...",
|
"messagePart.actions.deleting": "Deleting...",
|
||||||
"messagePart.actions.deleteTitle": "Delete this item",
|
"messagePart.actions.deleteTitle": "Delete this item",
|
||||||
"messagePart.actions.deleteFailedTitle": "Delete failed",
|
"messagePart.actions.deleteFailedTitle": "Delete failed",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Ir a sesión",
|
"messageBlock.tool.goToSession.label": "Ir a sesión",
|
||||||
"messageBlock.tool.goToSession.title": "Ir a la sesión",
|
"messageBlock.tool.goToSession.title": "Ir a la sesión",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "La sesión aún no está disponible",
|
"messageBlock.tool.goToSession.unavailableTitle": "La sesión aún no está disponible",
|
||||||
"messageBlock.tool.deletePart.label": "Eliminar",
|
"messageBlock.tool.deletePart.label": "Eliminar parte",
|
||||||
"messageBlock.tool.deletePart.deleting": "Eliminando...",
|
"messageBlock.tool.deletePart.deleting": "Eliminando...",
|
||||||
"messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta",
|
"messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta",
|
||||||
"messageBlock.tool.deletePart.failed.title": "Error al eliminar",
|
"messageBlock.tool.deletePart.failed.title": "Error al eliminar",
|
||||||
@@ -71,17 +71,21 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "Tú",
|
"messageItem.speaker.you": "Tú",
|
||||||
"messageItem.speaker.assistant": "Asistente",
|
"messageItem.speaker.assistant": "Asistente",
|
||||||
"messageItem.actions.revert": "Revertir",
|
"messageItem.actions.revert": "Revertir",
|
||||||
"messageItem.actions.revertTitle": "Revertir a este mensaje",
|
"messageItem.actions.revertTitle": "Deshacer cambios hasta aqui",
|
||||||
"messageItem.actions.fork": "Fork",
|
"messageItem.actions.fork": "Fork",
|
||||||
"messageItem.actions.forkTitle": "Fork desde este mensaje",
|
"messageItem.actions.forkTitle": "Fork desde este mensaje",
|
||||||
"messageItem.actions.copy": "Copiar",
|
"messageItem.actions.copy": "Copiar",
|
||||||
"messageItem.actions.copyTitle": "Copiar mensaje",
|
"messageItem.actions.copyTitle": "Copiar mensaje",
|
||||||
"messageItem.actions.copied": "¡Copiado!",
|
"messageItem.actions.copied": "¡Copiado!",
|
||||||
|
"messageItem.actions.deleteMessage": "Eliminar mensaje",
|
||||||
|
"messageItem.actions.deletingMessage": "Eliminando...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "Error al eliminar",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "No se pudo eliminar el mensaje",
|
||||||
"messageItem.status.queued": "EN COLA",
|
"messageItem.status.queued": "EN COLA",
|
||||||
"messageItem.status.generating": "Generando...",
|
"messageItem.status.generating": "Generando...",
|
||||||
"messageItem.status.sending": "Enviando...",
|
"messageItem.status.sending": "Enviando...",
|
||||||
"messageItem.status.failedToSend": "No se pudo enviar el mensaje",
|
"messageItem.status.failedToSend": "No se pudo enviar el mensaje",
|
||||||
"messagePart.actions.delete": "Eliminar",
|
"messagePart.actions.delete": "Eliminar parte",
|
||||||
"messagePart.actions.deleting": "Eliminando...",
|
"messagePart.actions.deleting": "Eliminando...",
|
||||||
"messagePart.actions.deleteTitle": "Eliminar este elemento",
|
"messagePart.actions.deleteTitle": "Eliminar este elemento",
|
||||||
"messagePart.actions.deleteFailedTitle": "Error al eliminar",
|
"messagePart.actions.deleteFailedTitle": "Error al eliminar",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Aller à la session",
|
"messageBlock.tool.goToSession.label": "Aller à la session",
|
||||||
"messageBlock.tool.goToSession.title": "Aller à la session",
|
"messageBlock.tool.goToSession.title": "Aller à la session",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "Session pas encore disponible",
|
"messageBlock.tool.goToSession.unavailableTitle": "Session pas encore disponible",
|
||||||
"messageBlock.tool.deletePart.label": "Supprimer",
|
"messageBlock.tool.deletePart.label": "Supprimer la partie",
|
||||||
"messageBlock.tool.deletePart.deleting": "Suppression...",
|
"messageBlock.tool.deletePart.deleting": "Suppression...",
|
||||||
"messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil",
|
"messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil",
|
||||||
"messageBlock.tool.deletePart.failed.title": "Échec de suppression",
|
"messageBlock.tool.deletePart.failed.title": "Échec de suppression",
|
||||||
@@ -71,17 +71,21 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "Vous",
|
"messageItem.speaker.you": "Vous",
|
||||||
"messageItem.speaker.assistant": "Assistant",
|
"messageItem.speaker.assistant": "Assistant",
|
||||||
"messageItem.actions.revert": "Revenir",
|
"messageItem.actions.revert": "Revenir",
|
||||||
"messageItem.actions.revertTitle": "Revenir à ce message",
|
"messageItem.actions.revertTitle": "Annuler les changements jusqu'ici",
|
||||||
"messageItem.actions.fork": "Fork",
|
"messageItem.actions.fork": "Fork",
|
||||||
"messageItem.actions.forkTitle": "Fork depuis ce message",
|
"messageItem.actions.forkTitle": "Fork depuis ce message",
|
||||||
"messageItem.actions.copy": "Copier",
|
"messageItem.actions.copy": "Copier",
|
||||||
"messageItem.actions.copyTitle": "Copier le message",
|
"messageItem.actions.copyTitle": "Copier le message",
|
||||||
"messageItem.actions.copied": "Copié !",
|
"messageItem.actions.copied": "Copié !",
|
||||||
|
"messageItem.actions.deleteMessage": "Supprimer le message",
|
||||||
|
"messageItem.actions.deletingMessage": "Suppression...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "Échec de suppression",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "Impossible de supprimer le message",
|
||||||
"messageItem.status.queued": "EN FILE",
|
"messageItem.status.queued": "EN FILE",
|
||||||
"messageItem.status.generating": "Génération...",
|
"messageItem.status.generating": "Génération...",
|
||||||
"messageItem.status.sending": "Envoi...",
|
"messageItem.status.sending": "Envoi...",
|
||||||
"messageItem.status.failedToSend": "Échec de l'envoi du message",
|
"messageItem.status.failedToSend": "Échec de l'envoi du message",
|
||||||
"messagePart.actions.delete": "Supprimer",
|
"messagePart.actions.delete": "Supprimer la partie",
|
||||||
"messagePart.actions.deleting": "Suppression...",
|
"messagePart.actions.deleting": "Suppression...",
|
||||||
"messagePart.actions.deleteTitle": "Supprimer cet élément",
|
"messagePart.actions.deleteTitle": "Supprimer cet élément",
|
||||||
"messagePart.actions.deleteFailedTitle": "Échec de suppression",
|
"messagePart.actions.deleteFailedTitle": "Échec de suppression",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "セッションへ移動",
|
"messageBlock.tool.goToSession.label": "セッションへ移動",
|
||||||
"messageBlock.tool.goToSession.title": "セッションへ移動",
|
"messageBlock.tool.goToSession.title": "セッションへ移動",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません",
|
"messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません",
|
||||||
"messageBlock.tool.deletePart.label": "削除",
|
"messageBlock.tool.deletePart.label": "パートを削除",
|
||||||
"messageBlock.tool.deletePart.deleting": "削除中...",
|
"messageBlock.tool.deletePart.deleting": "削除中...",
|
||||||
"messageBlock.tool.deletePart.title": "このツール出力を削除",
|
"messageBlock.tool.deletePart.title": "このツール出力を削除",
|
||||||
"messageBlock.tool.deletePart.failed.title": "削除に失敗しました",
|
"messageBlock.tool.deletePart.failed.title": "削除に失敗しました",
|
||||||
@@ -71,17 +71,21 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "あなた",
|
"messageItem.speaker.you": "あなた",
|
||||||
"messageItem.speaker.assistant": "アシスタント",
|
"messageItem.speaker.assistant": "アシスタント",
|
||||||
"messageItem.actions.revert": "戻す",
|
"messageItem.actions.revert": "戻す",
|
||||||
"messageItem.actions.revertTitle": "このメッセージまで戻す",
|
"messageItem.actions.revertTitle": "ここまでの変更を元に戻す",
|
||||||
"messageItem.actions.fork": "フォーク",
|
"messageItem.actions.fork": "フォーク",
|
||||||
"messageItem.actions.forkTitle": "このメッセージからフォーク",
|
"messageItem.actions.forkTitle": "このメッセージからフォーク",
|
||||||
"messageItem.actions.copy": "コピー",
|
"messageItem.actions.copy": "コピー",
|
||||||
"messageItem.actions.copyTitle": "メッセージをコピー",
|
"messageItem.actions.copyTitle": "メッセージをコピー",
|
||||||
"messageItem.actions.copied": "コピーしました!",
|
"messageItem.actions.copied": "コピーしました!",
|
||||||
|
"messageItem.actions.deleteMessage": "メッセージを削除",
|
||||||
|
"messageItem.actions.deletingMessage": "削除中...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "削除に失敗しました",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "メッセージの削除に失敗しました",
|
||||||
"messageItem.status.queued": "待機中",
|
"messageItem.status.queued": "待機中",
|
||||||
"messageItem.status.generating": "生成中...",
|
"messageItem.status.generating": "生成中...",
|
||||||
"messageItem.status.sending": "送信中...",
|
"messageItem.status.sending": "送信中...",
|
||||||
"messageItem.status.failedToSend": "メッセージの送信に失敗しました",
|
"messageItem.status.failedToSend": "メッセージの送信に失敗しました",
|
||||||
"messagePart.actions.delete": "削除",
|
"messagePart.actions.delete": "パートを削除",
|
||||||
"messagePart.actions.deleting": "削除中...",
|
"messagePart.actions.deleting": "削除中...",
|
||||||
"messagePart.actions.deleteTitle": "この項目を削除",
|
"messagePart.actions.deleteTitle": "この項目を削除",
|
||||||
"messagePart.actions.deleteFailedTitle": "削除に失敗しました",
|
"messagePart.actions.deleteFailedTitle": "削除に失敗しました",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Перейти к сессии",
|
"messageBlock.tool.goToSession.label": "Перейти к сессии",
|
||||||
"messageBlock.tool.goToSession.title": "Перейти к сессии",
|
"messageBlock.tool.goToSession.title": "Перейти к сессии",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна",
|
"messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна",
|
||||||
"messageBlock.tool.deletePart.label": "Удалить",
|
"messageBlock.tool.deletePart.label": "Удалить часть",
|
||||||
"messageBlock.tool.deletePart.deleting": "Удаление...",
|
"messageBlock.tool.deletePart.deleting": "Удаление...",
|
||||||
"messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента",
|
"messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента",
|
||||||
"messageBlock.tool.deletePart.failed.title": "Ошибка удаления",
|
"messageBlock.tool.deletePart.failed.title": "Ошибка удаления",
|
||||||
@@ -71,17 +71,21 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "Вы",
|
"messageItem.speaker.you": "Вы",
|
||||||
"messageItem.speaker.assistant": "Ассистент",
|
"messageItem.speaker.assistant": "Ассистент",
|
||||||
"messageItem.actions.revert": "Откатить",
|
"messageItem.actions.revert": "Откатить",
|
||||||
"messageItem.actions.revertTitle": "Откатиться к этому сообщению",
|
"messageItem.actions.revertTitle": "Отменить изменения до этого места",
|
||||||
"messageItem.actions.fork": "Форк",
|
"messageItem.actions.fork": "Форк",
|
||||||
"messageItem.actions.forkTitle": "Форкнуть от этого сообщения",
|
"messageItem.actions.forkTitle": "Форкнуть от этого сообщения",
|
||||||
"messageItem.actions.copy": "Копировать",
|
"messageItem.actions.copy": "Копировать",
|
||||||
"messageItem.actions.copyTitle": "Копировать сообщение",
|
"messageItem.actions.copyTitle": "Копировать сообщение",
|
||||||
"messageItem.actions.copied": "Скопировано!",
|
"messageItem.actions.copied": "Скопировано!",
|
||||||
|
"messageItem.actions.deleteMessage": "Удалить сообщение",
|
||||||
|
"messageItem.actions.deletingMessage": "Удаление...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "Ошибка удаления",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "Не удалось удалить сообщение",
|
||||||
"messageItem.status.queued": "В ОЧЕРЕДИ",
|
"messageItem.status.queued": "В ОЧЕРЕДИ",
|
||||||
"messageItem.status.generating": "Генерация…",
|
"messageItem.status.generating": "Генерация…",
|
||||||
"messageItem.status.sending": "Отправка…",
|
"messageItem.status.sending": "Отправка…",
|
||||||
"messageItem.status.failedToSend": "Не удалось отправить сообщение",
|
"messageItem.status.failedToSend": "Не удалось отправить сообщение",
|
||||||
"messagePart.actions.delete": "Удалить",
|
"messagePart.actions.delete": "Удалить часть",
|
||||||
"messagePart.actions.deleting": "Удаление...",
|
"messagePart.actions.deleting": "Удаление...",
|
||||||
"messagePart.actions.deleteTitle": "Удалить этот элемент",
|
"messagePart.actions.deleteTitle": "Удалить этот элемент",
|
||||||
"messagePart.actions.deleteFailedTitle": "Ошибка удаления",
|
"messagePart.actions.deleteFailedTitle": "Ошибка удаления",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "前往会话",
|
"messageBlock.tool.goToSession.label": "前往会话",
|
||||||
"messageBlock.tool.goToSession.title": "前往会话",
|
"messageBlock.tool.goToSession.title": "前往会话",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用",
|
"messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用",
|
||||||
"messageBlock.tool.deletePart.label": "删除",
|
"messageBlock.tool.deletePart.label": "删除部分",
|
||||||
"messageBlock.tool.deletePart.deleting": "正在删除...",
|
"messageBlock.tool.deletePart.deleting": "正在删除...",
|
||||||
"messageBlock.tool.deletePart.title": "删除此工具输出",
|
"messageBlock.tool.deletePart.title": "删除此工具输出",
|
||||||
"messageBlock.tool.deletePart.failed.title": "删除失败",
|
"messageBlock.tool.deletePart.failed.title": "删除失败",
|
||||||
@@ -71,17 +71,21 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "你",
|
"messageItem.speaker.you": "你",
|
||||||
"messageItem.speaker.assistant": "助手",
|
"messageItem.speaker.assistant": "助手",
|
||||||
"messageItem.actions.revert": "回退",
|
"messageItem.actions.revert": "回退",
|
||||||
"messageItem.actions.revertTitle": "回退到这条消息",
|
"messageItem.actions.revertTitle": "撤销到此处的更改",
|
||||||
"messageItem.actions.fork": "分叉",
|
"messageItem.actions.fork": "分叉",
|
||||||
"messageItem.actions.forkTitle": "从这条消息分叉",
|
"messageItem.actions.forkTitle": "从这条消息分叉",
|
||||||
"messageItem.actions.copy": "复制",
|
"messageItem.actions.copy": "复制",
|
||||||
"messageItem.actions.copyTitle": "复制消息",
|
"messageItem.actions.copyTitle": "复制消息",
|
||||||
"messageItem.actions.copied": "已复制!",
|
"messageItem.actions.copied": "已复制!",
|
||||||
|
"messageItem.actions.deleteMessage": "删除消息",
|
||||||
|
"messageItem.actions.deletingMessage": "正在删除...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "删除失败",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "无法删除消息",
|
||||||
"messageItem.status.queued": "排队中",
|
"messageItem.status.queued": "排队中",
|
||||||
"messageItem.status.generating": "正在生成...",
|
"messageItem.status.generating": "正在生成...",
|
||||||
"messageItem.status.sending": "正在发送...",
|
"messageItem.status.sending": "正在发送...",
|
||||||
"messageItem.status.failedToSend": "消息发送失败",
|
"messageItem.status.failedToSend": "消息发送失败",
|
||||||
"messagePart.actions.delete": "删除",
|
"messagePart.actions.delete": "删除部分",
|
||||||
"messagePart.actions.deleting": "正在删除...",
|
"messagePart.actions.deleting": "正在删除...",
|
||||||
"messagePart.actions.deleteTitle": "删除此项",
|
"messagePart.actions.deleteTitle": "删除此项",
|
||||||
"messagePart.actions.deleteFailedTitle": "删除失败",
|
"messagePart.actions.deleteFailedTitle": "删除失败",
|
||||||
|
|||||||
@@ -127,17 +127,23 @@ async function ensureLanguages(content: string) {
|
|||||||
if (highlightSuppressed) {
|
if (highlightSuppressed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Parse code fences to extract language tokens
|
|
||||||
// Updated regex to capture optional language tokens and handle trailing annotations
|
|
||||||
const codeBlockRegex = /```[ \t]*([A-Za-z0-9_.+#-]+)?[^`]*?```/g
|
|
||||||
const foundLanguages = new Set<string>()
|
|
||||||
let match
|
|
||||||
|
|
||||||
while ((match = codeBlockRegex.exec(content)) !== null) {
|
// Extract code-fence language tokens via `marked` so we correctly handle code blocks
|
||||||
const langToken = match[1]
|
// that contain backticks (e.g. JS template literals). Regex-based fence scans tend
|
||||||
if (langToken && langToken.trim()) {
|
// to miss these and prevent languages from loading.
|
||||||
foundLanguages.add(langToken.trim())
|
const foundLanguages = new Set<string>()
|
||||||
}
|
try {
|
||||||
|
const tokens = marked.lexer(content) as any
|
||||||
|
marked.walkTokens(tokens, (token: any) => {
|
||||||
|
if (token?.type !== "code") return
|
||||||
|
const langToken = typeof token.lang === "string" ? token.lang : ""
|
||||||
|
if (langToken.trim()) {
|
||||||
|
foundLanguages.add(langToken.trim())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// If tokenization fails for any reason, skip language preloading.
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue language loading tasks
|
// Queue language loading tasks
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { providers, sessions, withSession } from "./session-state"
|
|||||||
import { getDefaultModel, isModelValid } from "./session-models"
|
import { getDefaultModel, isModelValid } from "./session-models"
|
||||||
import { updateSessionInfo } from "./message-v2/session-info"
|
import { updateSessionInfo } from "./message-v2/session-info"
|
||||||
import { messageStoreBus } from "./message-v2/bus"
|
import { messageStoreBus } from "./message-v2/bus"
|
||||||
import { removeMessagePartV2 } from "./message-v2/bridge"
|
import { removeMessagePartV2, removeMessageV2 } from "./message-v2/bridge"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { requestData } from "../lib/opencode-api"
|
import { requestData } from "../lib/opencode-api"
|
||||||
|
|
||||||
@@ -439,8 +439,33 @@ async function deleteMessagePart(instanceId: string, sessionId: string, messageI
|
|||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteMessage(instanceId: string, sessionId: string, messageId: string): Promise<void> {
|
||||||
|
if (!instanceId || !sessionId || !messageId) return
|
||||||
|
const instance = instances().get(instanceId)
|
||||||
|
if (!instance || !instance.client) {
|
||||||
|
throw new Error("Instance not ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
|
||||||
|
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
||||||
|
|
||||||
|
// The SDK generator does not currently expose a typed method for deleting a message,
|
||||||
|
// but the API is available at DELETE /session/:sessionID/message/:messageID.
|
||||||
|
await requestData(
|
||||||
|
(client as any).client.delete({
|
||||||
|
url: `/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`,
|
||||||
|
}),
|
||||||
|
"session.message.delete",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Optimistic removal; SSE will also broadcast a message-removed event.
|
||||||
|
removeMessageV2(instanceId, messageId)
|
||||||
|
updateSessionInfo(instanceId, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
abortSession,
|
abortSession,
|
||||||
|
deleteMessage,
|
||||||
deleteMessagePart,
|
deleteMessagePart,
|
||||||
executeCustomCommand,
|
executeCustomCommand,
|
||||||
renameSession,
|
renameSession,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@import "./messaging/prompt-input.css";
|
@import "./messaging/prompt-input.css";
|
||||||
@import "./messaging/message-section.css";
|
@import "./messaging/message-section.css";
|
||||||
@import "./messaging/message-block-list.css";
|
@import "./messaging/message-block-list.css";
|
||||||
|
@import "./messaging/delete-overlays.css";
|
||||||
@import "./messaging/message-timeline.css";
|
@import "./messaging/message-timeline.css";
|
||||||
@import "./messaging/tool-call.css";
|
@import "./messaging/tool-call.css";
|
||||||
@import "./messaging/log-view.css";
|
@import "./messaging/log-view.css";
|
||||||
@@ -110,4 +111,3 @@
|
|||||||
.reasoning-label {
|
.reasoning-label {
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
packages/ui/src/styles/messaging/delete-overlays.css
Normal file
37
packages/ui/src/styles/messaging/delete-overlays.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* Hover overlays for destructive actions (delete part / delete message). */
|
||||||
|
|
||||||
|
.message-stream-block[data-delete-message-hover="true"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-stream-block[data-delete-message-hover="true"]::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
background: var(--status-error-bg);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
||||||
|
border-radius: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
/* Overlay must sit above the message cards (they have opaque backgrounds). */
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-part-shell {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-hover-scope[data-delete-part-hover="true"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-hover-scope[data-delete-part-hover="true"]::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
background: var(--status-error-bg);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
||||||
|
border-radius: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
/* Overlay must sit above the part card background. */
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
@@ -87,6 +87,7 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--border-base);
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -98,6 +99,36 @@
|
|||||||
transition: transform 0.15s ease, background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.15s ease, background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-timeline-segment[data-delete-hover="true"]::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--status-error-bg);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
||||||
|
border-radius: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure delete hover is visible even on the active segment styling. */
|
||||||
|
.message-timeline-segment[data-delete-hover="true"],
|
||||||
|
.message-timeline-segment[data-delete-hover="true"]:hover,
|
||||||
|
.message-timeline-segment[data-delete-hover="true"]:focus-visible,
|
||||||
|
.message-timeline-segment-active[data-delete-hover="true"],
|
||||||
|
.message-timeline-segment-active[data-delete-hover="true"]:hover,
|
||||||
|
.message-timeline-segment-active[data-delete-hover="true"]:focus-visible {
|
||||||
|
/* Let the ::before overlay provide the highlight (matches stream behavior). */
|
||||||
|
background-color: transparent !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-timeline-label {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.message-timeline-segment.message-timeline-segment-hidden {
|
.message-timeline-segment.message-timeline-segment-hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
4
packages/ui/src/types/delete-hover.ts
Normal file
4
packages/ui/src/types/delete-hover.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type DeleteHoverState =
|
||||||
|
| { kind: "none" }
|
||||||
|
| { kind: "message"; messageId: string }
|
||||||
|
| { kind: "part"; messageId: string; partId: string; partType?: string }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// SDK types
|
// SDK v2 types
|
||||||
import type {
|
import type {
|
||||||
EventMessageUpdated as MessageUpdateEvent,
|
EventMessageUpdated as MessageUpdateEvent,
|
||||||
EventMessageRemoved as MessageRemovedEvent,
|
EventMessageRemoved as MessageRemovedEvent,
|
||||||
@@ -6,7 +6,8 @@ import type {
|
|||||||
EventMessagePartRemoved as MessagePartRemovedEvent,
|
EventMessagePartRemoved as MessagePartRemovedEvent,
|
||||||
Part as SDKPart,
|
Part as SDKPart,
|
||||||
Message as SDKMessage,
|
Message as SDKMessage,
|
||||||
} from "@opencode-ai/sdk"
|
AssistantMessage as SDKAssistantMessageV2,
|
||||||
|
} from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
import type { PermissionRequestLike } from "./permission"
|
import type { PermissionRequestLike } from "./permission"
|
||||||
|
|
||||||
@@ -17,7 +18,8 @@ export type {
|
|||||||
MessagePartUpdatedEvent,
|
MessagePartUpdatedEvent,
|
||||||
MessagePartRemovedEvent,
|
MessagePartRemovedEvent,
|
||||||
SDKPart,
|
SDKPart,
|
||||||
SDKMessage
|
SDKMessage,
|
||||||
|
SDKAssistantMessageV2,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server streaming event: append-only delta updates.
|
// Server streaming event: append-only delta updates.
|
||||||
|
|||||||
Reference in New Issue
Block a user