mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(web): trim redundant and incorrect tooltips (#1316)
* fix(web): trim redundant and incorrect tooltips Drop hover tooltips that only restated a button's accessible label, and remove the permission-pill tooltip that described cycling modes while the control actually opens a dropdown. Keep tooltips that reveal truncated text or explain status. * fix(web): remove remaining ChatHeader tooltips Drop the session-title, git-branch, and open-PR tooltips in ChatHeader so the header has no hover tooltips. Broaden the changeset wording to cover the wider trim. * fix(web): keep tooltips from getting stuck on unmount Tooltip attached its mouseleave listener to the slotted trigger element once, so if that element was removed (e.g. a v-if toggled while hovering a tool-call path) the open bubble never received mouseleave and stranded on screen. Re-sync the listener to the live slotted element via a MutationObserver and close the tooltip whenever the trigger changes. * chore: add changeset for stuck-tooltip fix * fix(web): restore session title tooltip in ChatHeader The session title is truncated with an ellipsis when it exceeds the header width, so the tooltip was the only way to read the full name from the header. Restoring it keeps the truncation-revealing hint while the redundant git/branch/open-PR tooltips stay removed.
This commit is contained in:
parent
78a058acd2
commit
5322c63889
18 changed files with 353 additions and 406 deletions
5
.changeset/fix-stuck-web-tooltip.md
Normal file
5
.changeset/fix-stuck-web-tooltip.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix web tooltips that could get stuck on screen when their trigger element is removed while open.
|
||||
5
.changeset/trim-redundant-web-tooltips.md
Normal file
5
.changeset/trim-redundant-web-tooltips.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Trim redundant and incorrect tooltips in the web UI.
|
||||
|
|
@ -40,7 +40,6 @@ import type { AppConfig, ThinkingLevel } from './api/types';
|
|||
import Button from './components/ui/Button.vue';
|
||||
import IconButton from './components/ui/IconButton.vue';
|
||||
import Icon from './components/ui/Icon.vue';
|
||||
import Tooltip from './components/ui/Tooltip.vue';
|
||||
|
||||
// Hydrate the server-transport credential (fragment token or sessionStorage)
|
||||
// BEFORE the client connects, so the first REST/WS calls already carry it.
|
||||
|
|
@ -625,15 +624,13 @@ function openPr(url: string): void {
|
|||
@update:width="sessionColWidth = $event"
|
||||
/>
|
||||
<div v-if="sidebarCollapsed" class="sidebar-rail">
|
||||
<Tooltip :text="t('sidebar.expandSidebar')">
|
||||
<IconButton
|
||||
size="sm"
|
||||
:label="t('sidebar.expandSidebar')"
|
||||
@click="toggleSidebarCollapse"
|
||||
>
|
||||
<Icon name="panel-expand" size="sm" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
size="sm"
|
||||
:label="t('sidebar.expandSidebar')"
|
||||
@click="toggleSidebarCollapse"
|
||||
>
|
||||
<Icon name="panel-expand" size="sm" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -482,60 +482,46 @@ function truncatePath(path: string, maxLen = 55): string {
|
|||
<span v-if="searchQuery.trim()" class="fp-search-count">
|
||||
{{ searchMatches.length }}
|
||||
</span>
|
||||
<Tooltip :text="t('filePreview.prevMatch')">
|
||||
<IconButton size="sm" :disabled="searchMatches.length === 0" :label="t('filePreview.prevMatch')" @click="nextMatch(-1)">
|
||||
<Icon name="arrow-up" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip :text="t('filePreview.nextMatch')">
|
||||
<IconButton size="sm" :disabled="searchMatches.length === 0" :label="t('filePreview.nextMatch')" @click="nextMatch(1)">
|
||||
<Icon name="arrow-down" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton size="sm" :disabled="searchMatches.length === 0" :label="t('filePreview.prevMatch')" @click="nextMatch(-1)">
|
||||
<Icon name="arrow-up" size="md" />
|
||||
</IconButton>
|
||||
<IconButton size="sm" :disabled="searchMatches.length === 0" :label="t('filePreview.nextMatch')" @click="nextMatch(1)">
|
||||
<Icon name="arrow-down" size="md" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<!-- Icon actions: text labels made the header wrap to two rows at the
|
||||
default panel width — icons + title tooltips keep it single-line. -->
|
||||
<Tooltip :text="copiedPath ? t('filePreview.copied') : t('filePreview.copyPath')">
|
||||
<IconButton size="sm" :class="{ copied: copiedPath }" :label="copiedPath ? t('filePreview.copied') : t('filePreview.copyPath')" @click="copyPath">
|
||||
<Icon v-if="!copiedPath" name="link" size="md" />
|
||||
<Icon v-else class="fp-check" name="check" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip :text="t('filePreview.openInEditor')">
|
||||
<IconButton v-if="externalActions" size="sm" :label="t('filePreview.openInEditor')" @click="emit('openExternal')">
|
||||
<Icon name="external-link" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip :text="t('filePreview.reveal')">
|
||||
<IconButton v-if="externalActions" size="sm" :label="t('filePreview.reveal')" @click="emit('reveal')">
|
||||
<Icon name="folder" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip :text="t('filePreview.download')">
|
||||
<a
|
||||
v-if="downloadUrl"
|
||||
class="fp-download"
|
||||
:href="downloadUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
download
|
||||
:aria-label="t('filePreview.download')"
|
||||
>
|
||||
<Icon name="download" size="md" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip :text="copied ? t('filePreview.copied') : t('filePreview.copy')">
|
||||
<IconButton
|
||||
v-if="!file.isBinary && contentKind !== 'image'"
|
||||
size="sm"
|
||||
:class="{ copied }"
|
||||
:label="copied ? t('filePreview.copied') : t('filePreview.copy')"
|
||||
@click="copyContent"
|
||||
>
|
||||
<Icon v-if="!copied" name="copy" size="md" />
|
||||
<Icon v-else class="fp-check" name="check" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
default panel width — icon-only buttons keep it single-line. -->
|
||||
<IconButton size="sm" :class="{ copied: copiedPath }" :label="copiedPath ? t('filePreview.copied') : t('filePreview.copyPath')" @click="copyPath">
|
||||
<Icon v-if="!copiedPath" name="link" size="md" />
|
||||
<Icon v-else class="fp-check" name="check" size="md" />
|
||||
</IconButton>
|
||||
<IconButton v-if="externalActions" size="sm" :label="t('filePreview.openInEditor')" @click="emit('openExternal')">
|
||||
<Icon name="external-link" size="md" />
|
||||
</IconButton>
|
||||
<IconButton v-if="externalActions" size="sm" :label="t('filePreview.reveal')" @click="emit('reveal')">
|
||||
<Icon name="folder" size="md" />
|
||||
</IconButton>
|
||||
<a
|
||||
v-if="downloadUrl"
|
||||
class="fp-download"
|
||||
:href="downloadUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
download
|
||||
:aria-label="t('filePreview.download')"
|
||||
>
|
||||
<Icon name="download" size="md" />
|
||||
</a>
|
||||
<IconButton
|
||||
v-if="!file.isBinary && contentKind !== 'image'"
|
||||
size="sm"
|
||||
:class="{ copied }"
|
||||
:label="copied ? t('filePreview.copied') : t('filePreview.copy')"
|
||||
@click="copyContent"
|
||||
>
|
||||
<Icon v-if="!copied" name="copy" size="md" />
|
||||
<Icon v-else class="fp-check" name="check" size="md" />
|
||||
</IconButton>
|
||||
</PanelHeader>
|
||||
|
||||
<!-- Body: Markdown -->
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import IconButton from './ui/IconButton.vue';
|
|||
import Icon from './ui/Icon.vue';
|
||||
import Menu from './ui/Menu.vue';
|
||||
import MenuItem from './ui/MenuItem.vue';
|
||||
import Tooltip from './ui/Tooltip.vue';
|
||||
import { useConfirmDialog } from '../composables/useConfirmDialog';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
@ -553,24 +552,20 @@ onBeforeUnmount(() => {
|
|||
<span class="ch-name">Kimi Code<span v-if="isDev" class="ch-endpoint"> · {{ endpoint }}</span></span>
|
||||
<InternalBuildBanner />
|
||||
</div>
|
||||
<Tooltip :text="t('sidebar.collapseSidebar')">
|
||||
<IconButton
|
||||
size="sm"
|
||||
:label="t('sidebar.collapseSidebar')"
|
||||
@click.stop="emit('collapse')"
|
||||
>
|
||||
<Icon name="panel-collapse" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip :text="t('settings.title')">
|
||||
<IconButton
|
||||
size="sm"
|
||||
:label="t('settings.title')"
|
||||
@click.stop="emit('openSettings')"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
size="sm"
|
||||
:label="t('sidebar.collapseSidebar')"
|
||||
@click.stop="emit('collapse')"
|
||||
>
|
||||
<Icon name="panel-collapse" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="sm"
|
||||
:label="t('settings.title')"
|
||||
@click.stop="emit('openSettings')"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<!-- Session search — opens the Spotlight-style search dialog -->
|
||||
|
|
@ -585,16 +580,14 @@ onBeforeUnmount(() => {
|
|||
<Icon name="chat-new" />
|
||||
<span>{{ t('sidebar.newChat') }}</span>
|
||||
</button>
|
||||
<Tooltip :text="t('sidebar.newWorkspace')">
|
||||
<IconButton
|
||||
v-if="showNewWorkspaceButton"
|
||||
size="sm"
|
||||
:label="t('sidebar.newWorkspace')"
|
||||
@click.stop="emit('addWorkspace')"
|
||||
>
|
||||
<Icon name="folder" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
v-if="showNewWorkspaceButton"
|
||||
size="sm"
|
||||
:label="t('sidebar.newWorkspace')"
|
||||
@click.stop="emit('addWorkspace')"
|
||||
>
|
||||
<Icon name="folder" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<!-- Session list — grouped by workspace -->
|
||||
|
|
@ -609,29 +602,25 @@ onBeforeUnmount(() => {
|
|||
<div class="side-section-label">
|
||||
<span class="side-section-title">{{ t('sidebar.workspaces') }}</span>
|
||||
<div class="side-section-actions">
|
||||
<Tooltip :text="allCollapsed ? t('sidebar.expandAll') : t('sidebar.collapseAll')">
|
||||
<IconButton
|
||||
class="side-section-toggle"
|
||||
size="sm"
|
||||
:label="allCollapsed ? t('sidebar.expandAll') : t('sidebar.collapseAll')"
|
||||
@click.stop="allCollapsed ? expandAllWorkspaces() : collapseAllWorkspaces()"
|
||||
>
|
||||
<Icon v-if="allCollapsed" name="expand" />
|
||||
<Icon v-else name="collapse" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip :text="t('sidebar.options')">
|
||||
<IconButton
|
||||
class="side-section-toggle side-section-kebab"
|
||||
size="sm"
|
||||
:label="t('sidebar.options')"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="sectionMenuOpen"
|
||||
@click.stop="toggleSectionMenu($event)"
|
||||
>
|
||||
<Icon name="dots-horizontal" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
class="side-section-toggle"
|
||||
size="sm"
|
||||
:label="allCollapsed ? t('sidebar.expandAll') : t('sidebar.collapseAll')"
|
||||
@click.stop="allCollapsed ? expandAllWorkspaces() : collapseAllWorkspaces()"
|
||||
>
|
||||
<Icon v-if="allCollapsed" name="expand" />
|
||||
<Icon v-else name="collapse" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
class="side-section-toggle side-section-kebab"
|
||||
size="sm"
|
||||
:label="t('sidebar.options')"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="sectionMenuOpen"
|
||||
@click.stop="toggleSectionMenu($event)"
|
||||
>
|
||||
<Icon name="dots-horizontal" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -108,30 +108,26 @@ function onHeaderDragStart(event: DragEvent): void {
|
|||
@click.stop
|
||||
/>
|
||||
|
||||
<Tooltip :text="t('sidebar.options')">
|
||||
<IconButton
|
||||
class="gh-more"
|
||||
:class="{ open: wsMenuOpenId === group.workspace.id }"
|
||||
size="sm"
|
||||
:label="t('sidebar.options')"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="wsMenuOpenId === group.workspace.id"
|
||||
@click.stop="emit('toggleWsMenu', group.workspace, $event)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="sm" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
class="gh-more"
|
||||
:class="{ open: wsMenuOpenId === group.workspace.id }"
|
||||
size="sm"
|
||||
:label="t('sidebar.options')"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="wsMenuOpenId === group.workspace.id"
|
||||
@click.stop="emit('toggleWsMenu', group.workspace, $event)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="sm" />
|
||||
</IconButton>
|
||||
|
||||
<Tooltip :text="t('workspace.newInGroup')">
|
||||
<IconButton
|
||||
class="gh-add"
|
||||
size="sm"
|
||||
:label="t('workspace.newInGroup')"
|
||||
@click.stop="emit('createInWorkspace', group.workspace.id)"
|
||||
>
|
||||
<Icon name="chat-new" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
class="gh-add"
|
||||
size="sm"
|
||||
:label="t('workspace.newInGroup')"
|
||||
@click.stop="emit('createInWorkspace', group.workspace.id)"
|
||||
>
|
||||
<Icon name="chat-new" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<Tooltip :text="group.workspace.root">
|
||||
|
|
|
|||
|
|
@ -191,17 +191,15 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
</span>
|
||||
<Badge v-if="agentName && !minimized" variant="neutral" size="sm">{{ t('approval.subagentBadge', { name: agentName }) }}</Badge>
|
||||
<Badge v-if="!minimized" variant="warning" size="sm" class="aw">{{ t('approval.required') }}</Badge>
|
||||
<Tooltip :text="minimized ? t('question.expand') : t('question.minimize')">
|
||||
<IconButton
|
||||
class="amin"
|
||||
size="sm"
|
||||
:label="minimized ? t('question.expand') : t('question.minimize')"
|
||||
@click="minimized = !minimized"
|
||||
>
|
||||
<Icon v-if="minimized" name="chevron-down" size="md" />
|
||||
<Icon v-else name="minus" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
class="amin"
|
||||
size="sm"
|
||||
:label="minimized ? t('question.expand') : t('question.minimize')"
|
||||
@click="minimized = !minimized"
|
||||
>
|
||||
<Icon v-if="minimized" name="chevron-down" size="md" />
|
||||
<Icon v-else name="minus" size="md" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -222,19 +222,17 @@ async function startArchive(): Promise<void> {
|
|||
</div>
|
||||
|
||||
<!-- More menu trigger: copy-all + session actions -->
|
||||
<Tooltip :text="t('header.options')">
|
||||
<IconButton
|
||||
ref="kebabRef"
|
||||
class="ch-act-more"
|
||||
:class="{ open: menuOpen }"
|
||||
:label="t('header.options')"
|
||||
:aria-expanded="menuOpen"
|
||||
aria-haspopup="menu"
|
||||
@click.stop="toggleMenu($event)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
ref="kebabRef"
|
||||
class="ch-act-more"
|
||||
:class="{ open: menuOpen }"
|
||||
:label="t('header.options')"
|
||||
:aria-expanded="menuOpen"
|
||||
aria-haspopup="menu"
|
||||
@click.stop="toggleMenu($event)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="md" />
|
||||
</IconButton>
|
||||
|
||||
<!-- Fixed more menu -->
|
||||
<Menu
|
||||
|
|
@ -270,45 +268,39 @@ async function startArchive(): Promise<void> {
|
|||
<!-- Git branch + status — plain text with semantic colors. Renders for any
|
||||
git repo, even a detached HEAD (empty branch → "detached" label), so the
|
||||
diff counter below is never hidden just because there's no branch name. -->
|
||||
<Tooltip :text="t('header.gitTooltip')">
|
||||
<button
|
||||
v-if="isGitRepo"
|
||||
type="button"
|
||||
class="ch-git"
|
||||
@click="emit('openChanges')"
|
||||
<button
|
||||
v-if="isGitRepo"
|
||||
type="button"
|
||||
class="ch-git"
|
||||
@click="emit('openChanges')"
|
||||
>
|
||||
<span
|
||||
class="ch-branch"
|
||||
:class="{ 'ch-detached': !branch }"
|
||||
>
|
||||
<Tooltip :text="branch || t('header.detached')">
|
||||
<span
|
||||
class="ch-branch"
|
||||
:class="{ 'ch-detached': !branch }"
|
||||
>
|
||||
{{ branch || t('header.detached') }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span v-if="ahead > 0 || behind > 0" class="ch-pill ch-sync-pill">
|
||||
<span v-if="ahead > 0" class="ch-ahead">↑{{ ahead }}</span>
|
||||
<span v-if="behind > 0" class="ch-behind">↓{{ behind }}</span>
|
||||
</span>
|
||||
<span v-if="hasLineStats" class="ch-pill ch-diff-pill">
|
||||
<span v-if="adds > 0" class="ch-add">+{{ adds }}</span>
|
||||
<span v-if="dels > 0" class="ch-del">-{{ dels }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{{ branch || t('header.detached') }}
|
||||
</span>
|
||||
<span v-if="ahead > 0 || behind > 0" class="ch-pill ch-sync-pill">
|
||||
<span v-if="ahead > 0" class="ch-ahead">↑{{ ahead }}</span>
|
||||
<span v-if="behind > 0" class="ch-behind">↓{{ behind }}</span>
|
||||
</span>
|
||||
<span v-if="hasLineStats" class="ch-pill ch-diff-pill">
|
||||
<span v-if="adds > 0" class="ch-add">+{{ adds }}</span>
|
||||
<span v-if="dels > 0" class="ch-del">-{{ dels }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- GitHub PR status -->
|
||||
<Tooltip :text="t('header.openPr')">
|
||||
<button
|
||||
v-if="pr"
|
||||
type="button"
|
||||
class="ch-pill ch-pr"
|
||||
:class="`pr-${pr.state}`"
|
||||
@click="pr && emit('openPr', pr.url)"
|
||||
>
|
||||
<Icon name="git-pull-request" size="sm" />
|
||||
<span>PR #{{ pr.number }} · {{ pr.state }}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
v-if="pr"
|
||||
type="button"
|
||||
class="ch-pill ch-pr"
|
||||
:class="`pr-${pr.state}`"
|
||||
@click="pr && emit('openPr', pr.url)"
|
||||
>
|
||||
<Icon name="git-pull-request" size="sm" />
|
||||
<span>PR #{{ pr.number }} · {{ pr.state }}</span>
|
||||
</button>
|
||||
|
||||
</header>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -575,31 +575,25 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }):
|
|||
</div>
|
||||
<div v-if="turn.createdAt || canEditTurn(turn)" class="u-meta">
|
||||
<div v-if="canEditTurn(turn)" class="u-edit-wrap" :class="{ undoing: undoingTurnId === turn.id }">
|
||||
<Tooltip :text="t('conversation.undoTooltip')">
|
||||
<button
|
||||
type="button"
|
||||
class="u-edit"
|
||||
:aria-label="t('conversation.undoTooltip')"
|
||||
@click="onUndo(turn)"
|
||||
>
|
||||
<Icon name="undo" size="sm" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip
|
||||
v-if="turn.text.trim().length > 0"
|
||||
:text="t('filePreview.copy')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="u-copy"
|
||||
:aria-label="t('filePreview.copy')"
|
||||
@click.stop="copyUserMessage(turn)"
|
||||
class="u-edit"
|
||||
:aria-label="t('conversation.undoTooltip')"
|
||||
@click="onUndo(turn)"
|
||||
>
|
||||
<Icon v-if="copiedTurn !== turn.id" name="copy" size="sm" />
|
||||
<Icon v-else name="check" size="sm" />
|
||||
<Icon name="undo" size="sm" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<button
|
||||
v-if="turn.text.trim().length > 0"
|
||||
type="button"
|
||||
class="u-copy"
|
||||
:aria-label="t('filePreview.copy')"
|
||||
@click.stop="copyUserMessage(turn)"
|
||||
>
|
||||
<Icon v-if="copiedTurn !== turn.id" name="copy" size="sm" />
|
||||
<Icon v-else name="check" size="sm" />
|
||||
</button>
|
||||
<button
|
||||
v-if="turn.createdAt"
|
||||
type="button"
|
||||
|
|
@ -650,19 +644,15 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }):
|
|||
<Tooltip :text="`${turn.durationMs} ms`">
|
||||
<span v-if="turn.durationMs !== undefined" class="a-duration">{{ formatDuration(turn.durationMs) }}</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
<button
|
||||
v-if="assistantRunFinalText(ti).trim().length > 0"
|
||||
:text="t('filePreview.copy')"
|
||||
class="a-cpbtn"
|
||||
:aria-label="t('filePreview.copy')"
|
||||
@click="copyAssistantRun(ti)"
|
||||
>
|
||||
<button
|
||||
class="a-cpbtn"
|
||||
:aria-label="t('filePreview.copy')"
|
||||
@click="copyAssistantRun(ti)"
|
||||
>
|
||||
<Icon v-if="copiedTurn !== turn.id" name="copy" size="sm" />
|
||||
<Icon v-else name="check" size="sm" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Icon v-if="copiedTurn !== turn.id" name="copy" size="sm" />
|
||||
<Icon v-else name="check" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -733,16 +723,14 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }):
|
|||
</div>
|
||||
<span v-if="qi === 0" class="q-tag q-tag-next">{{ t('composer.queueNext') }}</span>
|
||||
<span v-else class="q-tag q-tag-idx">#{{ qi + 1 }}</span>
|
||||
<Tooltip :text="t('composer.remove')">
|
||||
<button
|
||||
type="button"
|
||||
class="q-rm"
|
||||
:aria-label="t('composer.remove')"
|
||||
@click.stop="emit('unqueue', qi)"
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
type="button"
|
||||
class="q-rm"
|
||||
:aria-label="t('composer.remove')"
|
||||
@click.stop="emit('unqueue', qi)"
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -783,18 +783,16 @@ function selectModel(modelId: string): void {
|
|||
@compositionend="handleCompositionEnd"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<Tooltip :text="expanded ? t('composer.collapseTitle') : t('composer.expandTitle')">
|
||||
<button
|
||||
v-if="expanded || isGrown"
|
||||
class="expand-btn"
|
||||
type="button"
|
||||
:aria-label="expanded ? t('composer.collapseTitle') : t('composer.expandTitle')"
|
||||
@click="toggleExpand"
|
||||
>
|
||||
<Icon v-if="expanded" name="collapse" size="sm" />
|
||||
<Icon v-else name="expand" size="sm" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
v-if="expanded || isGrown"
|
||||
class="expand-btn"
|
||||
type="button"
|
||||
:aria-label="expanded ? t('composer.collapseTitle') : t('composer.expandTitle')"
|
||||
@click="toggleExpand"
|
||||
>
|
||||
<Icon v-if="expanded" name="collapse" size="sm" />
|
||||
<Icon v-else name="expand" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -813,30 +811,26 @@ function selectModel(modelId: string): void {
|
|||
<div ref="toolbarRef" class="toolbar">
|
||||
<!-- Left: attach + permission + plan -->
|
||||
<div class="toolbar-left">
|
||||
<Tooltip :text="t('composer.attachImage')">
|
||||
<IconButton
|
||||
v-if="hasUpload"
|
||||
size="md"
|
||||
:label="t('composer.attachImage')"
|
||||
@click="openFilePicker"
|
||||
>
|
||||
<Icon name="image" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
v-if="hasUpload"
|
||||
size="md"
|
||||
:label="t('composer.attachImage')"
|
||||
@click="openFilePicker"
|
||||
>
|
||||
<Icon name="image" />
|
||||
</IconButton>
|
||||
|
||||
<!-- Permission pill — click to open dropdown -->
|
||||
<Tooltip :text="t('status.permissionTooltip')">
|
||||
<span
|
||||
v-if="status"
|
||||
class="perm-pill"
|
||||
:class="['perm-' + status.permission, { open: permDropdownOpen }]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click.stop="togglePermDropdown"
|
||||
@keydown.enter="togglePermDropdown"
|
||||
@keydown.space.prevent="togglePermDropdown"
|
||||
>{{ permLabel }}</span>
|
||||
</Tooltip>
|
||||
<span
|
||||
v-if="status"
|
||||
class="perm-pill"
|
||||
:class="['perm-' + status.permission, { open: permDropdownOpen }]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click.stop="togglePermDropdown"
|
||||
@keydown.enter="togglePermDropdown"
|
||||
@keydown.space.prevent="togglePermDropdown"
|
||||
>{{ permLabel }}</span>
|
||||
|
||||
<!-- Permission dropdown — anchored to the toolbar left side -->
|
||||
<div v-if="permDropdownOpen && status" class="perm-dropdown" role="menu" @click.stop>
|
||||
|
|
@ -858,19 +852,17 @@ function selectModel(modelId: string): void {
|
|||
|
||||
<!-- Modes selector (plan / goal / swarm) — replaces the plan pill. -->
|
||||
<div v-if="status" ref="modesRef" class="modes">
|
||||
<Tooltip :text="t('status.modesTooltip')">
|
||||
<button
|
||||
type="button"
|
||||
class="mode-pill"
|
||||
:class="{ on: anyModeActive, open: modesOpen }"
|
||||
@click.stop="toggleModes"
|
||||
>
|
||||
<span class="mode-label">{{ t('status.modesLabel') }}</span>
|
||||
<span v-if="planOn" class="mode-tag">{{ t('status.planLabel') }}</span>
|
||||
<span v-if="swarmOn" class="mode-tag">{{ t('status.swarmLabel') }}</span>
|
||||
<span v-if="goalArmed" class="mode-tag">{{ t('status.goalLabel') }}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
type="button"
|
||||
class="mode-pill"
|
||||
:class="{ on: anyModeActive, open: modesOpen }"
|
||||
@click.stop="toggleModes"
|
||||
>
|
||||
<span class="mode-label">{{ t('status.modesLabel') }}</span>
|
||||
<span v-if="planOn" class="mode-tag">{{ t('status.planLabel') }}</span>
|
||||
<span v-if="swarmOn" class="mode-tag">{{ t('status.swarmLabel') }}</span>
|
||||
<span v-if="goalArmed" class="mode-tag">{{ t('status.goalLabel') }}</span>
|
||||
</button>
|
||||
|
||||
<div v-if="modesOpen" ref="modesMenuRef" class="modes-menu" :style="modesMenuStyle">
|
||||
<!-- Plan — functional client toggle -->
|
||||
|
|
@ -925,22 +917,20 @@ function selectModel(modelId: string): void {
|
|||
</Tooltip>
|
||||
|
||||
<!-- Model pill — click to open quick-switch dropdown -->
|
||||
<Tooltip :text="t('status.modelTooltip')">
|
||||
<span
|
||||
v-if="status"
|
||||
class="model-pill"
|
||||
:class="{ open: dropdownOpen }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click.stop="toggleDropdown"
|
||||
@keydown.enter="toggleDropdown"
|
||||
@keydown.space.prevent="toggleDropdown"
|
||||
>
|
||||
<b>{{ status.model }}</b>
|
||||
<span v-if="thinkingOn" class="think-suffix">{{ t('composer.thinkingSuffix') }}</span>
|
||||
<Icon class="cv" name="chevron-down" size="sm" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span
|
||||
v-if="status"
|
||||
class="model-pill"
|
||||
:class="{ open: dropdownOpen }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click.stop="toggleDropdown"
|
||||
@keydown.enter="toggleDropdown"
|
||||
@keydown.space.prevent="toggleDropdown"
|
||||
>
|
||||
<b>{{ status.model }}</b>
|
||||
<span v-if="thinkingOn" class="think-suffix">{{ t('composer.thinkingSuffix') }}</span>
|
||||
<Icon class="cv" name="chevron-down" size="sm" />
|
||||
</span>
|
||||
<Tooltip v-if="running" :text="t('composer.interruptTitle')">
|
||||
<button
|
||||
class="stop"
|
||||
|
|
@ -950,15 +940,13 @@ function selectModel(modelId: string): void {
|
|||
<Icon name="stop" size="sm" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip :text="sendLabel">
|
||||
<button
|
||||
class="send"
|
||||
:aria-label="sendLabel"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
<Icon name="send" size="sm" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
class="send"
|
||||
:aria-label="sendLabel"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
<Icon name="send" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Model dropdown — current provider models + controls + more -->
|
||||
|
|
|
|||
|
|
@ -220,12 +220,10 @@ function treePadding(depth: number): string {
|
|||
</PanelHeader>
|
||||
|
||||
<div class="diff-head">
|
||||
<Tooltip :text="t('diff.back')">
|
||||
<Button v-if="!hideBack" variant="ghost" size="sm" @click="onBack">
|
||||
<span aria-hidden="true">←</span>
|
||||
<span class="back-label">{{ t('diff.back') }}</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button v-if="!hideBack" variant="ghost" size="sm" @click="onBack">
|
||||
<span aria-hidden="true">←</span>
|
||||
<span class="back-label">{{ t('diff.back') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="empty-state">{{ t('diff.loading') }}</div>
|
||||
|
|
|
|||
|
|
@ -198,18 +198,16 @@ async function copyPath(): Promise<void> {
|
|||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip :text="t('header.chooseOpenApp')">
|
||||
<IconButton
|
||||
ref="triggerRef"
|
||||
size="sm"
|
||||
:class="{ open: menuOpen }"
|
||||
:disabled="!hasWorkDir"
|
||||
:label="t('header.chooseOpenApp')"
|
||||
@click.stop="openMenu"
|
||||
>
|
||||
<Icon name="chevron-down" size="sm" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
ref="triggerRef"
|
||||
size="sm"
|
||||
:class="{ open: menuOpen }"
|
||||
:disabled="!hasWorkDir"
|
||||
:label="t('header.chooseOpenApp')"
|
||||
@click.stop="openMenu"
|
||||
>
|
||||
<Icon name="chevron-down" size="sm" />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
v-if="menuOpen"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import Badge from '../ui/Badge.vue';
|
|||
import Button from '../ui/Button.vue';
|
||||
import IconButton from '../ui/IconButton.vue';
|
||||
import Icon from '../ui/Icon.vue';
|
||||
import Tooltip from '../ui/Tooltip.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
question: UIQuestion;
|
||||
|
|
@ -278,17 +277,15 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
<span v-if="total > 1 && !minimized" class="qstep">{{ t('question.step', { current: step + 1, total }) }}</span>
|
||||
<!-- When minimized, surface the question text so the bar stays identifiable -->
|
||||
<span v-if="minimized" class="qmin-peek">{{ current.question }}</span>
|
||||
<Tooltip :text="minimized ? t('question.expand') : t('question.minimize')">
|
||||
<IconButton
|
||||
class="qmin"
|
||||
size="sm"
|
||||
:label="minimized ? t('question.expand') : t('question.minimize')"
|
||||
@click="minimized = !minimized"
|
||||
>
|
||||
<Icon v-if="minimized" name="chevron-down" size="md" />
|
||||
<Icon v-else name="minus" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
class="qmin"
|
||||
size="sm"
|
||||
:label="minimized ? t('question.expand') : t('question.minimize')"
|
||||
@click="minimized = !minimized"
|
||||
>
|
||||
<Icon v-if="minimized" name="chevron-down" size="md" />
|
||||
<Icon v-else name="minus" size="md" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -198,27 +198,23 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise<void> {
|
|||
class="att"
|
||||
>{{ wsAttention(g.workspace.id) }}</span>
|
||||
|
||||
<Tooltip :text="t('sidebar.options')">
|
||||
<IconButton
|
||||
size="lg"
|
||||
class="mgh-more"
|
||||
:label="t('sidebar.options')"
|
||||
@click.stop="toggleWsMenu(g.workspace.id)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
size="lg"
|
||||
class="mgh-more"
|
||||
:label="t('sidebar.options')"
|
||||
@click.stop="toggleWsMenu(g.workspace.id)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="md" />
|
||||
</IconButton>
|
||||
|
||||
<Tooltip :text="t('workspace.newInGroup')">
|
||||
<IconButton
|
||||
size="lg"
|
||||
class="mgh-add"
|
||||
:label="t('workspace.newInGroup')"
|
||||
@click.stop="onCreateInWorkspace(g.workspace.id)"
|
||||
>
|
||||
<Icon name="plus" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
size="lg"
|
||||
class="mgh-add"
|
||||
:label="t('workspace.newInGroup')"
|
||||
@click.stop="onCreateInWorkspace(g.workspace.id)"
|
||||
>
|
||||
<Icon name="plus" size="md" />
|
||||
</IconButton>
|
||||
|
||||
<!-- Workspace menu: copy path / delete (two-step confirm) -->
|
||||
<Menu v-if="wsMenuFor === g.workspace.id" class="kmenu wsmenu" @click.stop>
|
||||
|
|
@ -243,16 +239,14 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise<void> {
|
|||
<div class="s">{{ s.time }}</div>
|
||||
</div>
|
||||
<span v-if="(attentionBySession[s.id] ?? 0) > 0" class="att">{{ attentionBySession[s.id] }}</span>
|
||||
<Tooltip :text="t('sidebar.options')">
|
||||
<IconButton
|
||||
size="lg"
|
||||
class="kb"
|
||||
:label="t('sidebar.options')"
|
||||
@click.stop="toggleMenu(s.id)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="md" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
size="lg"
|
||||
class="kb"
|
||||
:label="t('sidebar.options')"
|
||||
@click.stop="toggleMenu(s.id)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="md" />
|
||||
</IconButton>
|
||||
|
||||
<!-- Kebab menu -->
|
||||
<Menu v-if="menuFor === s.id" class="kmenu" @click.stop>
|
||||
|
|
|
|||
|
|
@ -30,17 +30,15 @@ defineEmits<{ close: [] }>();
|
|||
<span v-if="subtitle" class="ui-panel-header__sub">{{ subtitle }}</span>
|
||||
</Tooltip>
|
||||
<slot />
|
||||
<Tooltip :text="closeLabel">
|
||||
<IconButton
|
||||
v-if="closable"
|
||||
class="ui-panel-header__close"
|
||||
size="sm"
|
||||
:label="closeLabel"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
v-if="closable"
|
||||
class="ui-panel-header__close"
|
||||
size="sm"
|
||||
:label="closeLabel"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
<!-- Design-system §03 Tooltip: hover/focus hint. Wrap the trigger in the default
|
||||
slot; text via prop. The wrapper is `display: contents` so it never alters the
|
||||
trigger's layout (safe for truncated/flex triggers); listeners are attached to
|
||||
the real trigger element, which also anchors the bubble. The bubble is rendered
|
||||
through a body teleport so it escapes ancestor overflow clipping, and positioned
|
||||
with flip + viewport clamping. Short text stays on one line; long text wraps
|
||||
within `maxWidth` and is clamped to `maxLines` lines with an ellipsis so the
|
||||
bubble never grows too tall. -->
|
||||
the real trigger element, which also anchors the bubble, and re-attached if that
|
||||
element is removed or replaced (so an open tooltip can never strand on screen).
|
||||
The bubble is rendered through a body teleport so it escapes ancestor overflow
|
||||
clipping, and positioned with flip + viewport clamping. Short text stays on one
|
||||
line; long text wraps within `maxWidth` and is clamped to `maxLines` lines with
|
||||
an ellipsis so the bubble never grows too tall. -->
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ const bubbleStyle = ref<Record<string, string>>({ maxWidth: `${props.maxWidth}px
|
|||
|
||||
let showTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let target: HTMLElement | null = null;
|
||||
let observer: MutationObserver | undefined;
|
||||
|
||||
function position(): void {
|
||||
const bub = bubble.value;
|
||||
|
|
@ -104,25 +106,47 @@ function onScrollOrResize(): void {
|
|||
if (open.value) hide();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
target = (trigger.value?.firstElementChild as HTMLElement | null) ?? trigger.value ?? null;
|
||||
if (!target) return;
|
||||
target.addEventListener('mouseenter', show);
|
||||
target.addEventListener('mouseleave', hide);
|
||||
target.addEventListener('focusin', show);
|
||||
target.addEventListener('focusout', hide);
|
||||
window.addEventListener('scroll', onScrollOrResize, true);
|
||||
window.addEventListener('resize', onScrollOrResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.clearTimeout(showTimer);
|
||||
function setTarget(el: HTMLElement | null): void {
|
||||
if (el === target) return;
|
||||
if (target) {
|
||||
target.removeEventListener('mouseenter', show);
|
||||
target.removeEventListener('mouseleave', hide);
|
||||
target.removeEventListener('focusin', show);
|
||||
target.removeEventListener('focusout', hide);
|
||||
}
|
||||
target = el;
|
||||
if (target) {
|
||||
target.addEventListener('mouseenter', show);
|
||||
target.addEventListener('mouseleave', hide);
|
||||
target.addEventListener('focusin', show);
|
||||
target.addEventListener('focusout', hide);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const root = trigger.value ?? null;
|
||||
setTarget((root?.firstElementChild as HTMLElement | null) ?? root);
|
||||
// Keep `target` in sync with the live slotted element: if it's removed or
|
||||
// replaced while the tooltip is open (e.g. a v-if toggles on hover), the
|
||||
// mouseleave we rely on never fires and the bubble would get stuck on screen.
|
||||
if (root) {
|
||||
observer = new MutationObserver(() => {
|
||||
const next = (root.firstElementChild as HTMLElement | null) ?? null;
|
||||
if (next !== target) {
|
||||
hide();
|
||||
setTarget(next ?? root);
|
||||
}
|
||||
});
|
||||
observer.observe(root, { childList: true });
|
||||
}
|
||||
window.addEventListener('scroll', onScrollOrResize, true);
|
||||
window.addEventListener('resize', onScrollOrResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.clearTimeout(showTimer);
|
||||
observer?.disconnect();
|
||||
setTarget(null);
|
||||
window.removeEventListener('scroll', onScrollOrResize, true);
|
||||
window.removeEventListener('resize', onScrollOrResize);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@ export default {
|
|||
connectionDisconnected: 'Disconnected',
|
||||
ctxTooltip: 'Used {used} / {max} tokens ({pct}%)',
|
||||
modelLabel: 'Model',
|
||||
modelTooltip: 'Click to switch model',
|
||||
permissionManual: 'Manual',
|
||||
permissionAuto: 'Auto',
|
||||
permissionYolo: 'YOLO',
|
||||
permissionTooltip: 'Click to cycle permission mode',
|
||||
permissionManualDesc: 'Ask for approval on every tool action',
|
||||
permissionAutoDesc: 'Fully autonomous — agent decides everything without asking',
|
||||
permissionYoloDesc: 'Auto-approve tool actions, but agent may still ask questions',
|
||||
|
|
@ -19,7 +17,6 @@ export default {
|
|||
planTooltip: 'Toggle plan mode (research before editing)',
|
||||
// Mode selector (Plan / Goal / Swarm)
|
||||
modesLabel: 'Mode',
|
||||
modesTooltip: 'Mode: Plan / Goal / Swarm',
|
||||
goalLabel: 'Goal',
|
||||
swarmLabel: 'Swarm',
|
||||
modeOff: 'Off',
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@ export default {
|
|||
connectionDisconnected: '未连接',
|
||||
ctxTooltip: '使用 {used} / {max} tokens ({pct}%)',
|
||||
modelLabel: '模型',
|
||||
modelTooltip: '点击切换模型',
|
||||
permissionManual: '逐条确认',
|
||||
permissionAuto: '完全自主',
|
||||
permissionYolo: '自动通过',
|
||||
permissionTooltip: '点击循环切换审批模式',
|
||||
permissionManualDesc: '每个工具操作都需要你手动确认',
|
||||
permissionAutoDesc: '完全自主运行,agent 自己做决定,不再询问',
|
||||
permissionYoloDesc: '自动批准工具操作,但遇到关键问题仍会询问',
|
||||
|
|
@ -19,7 +17,6 @@ export default {
|
|||
planTooltip: '切换计划模式(先调研再修改)',
|
||||
// 模式选择器(计划 / 目标 / Swarm)
|
||||
modesLabel: '模式',
|
||||
modesTooltip: '模式:计划 / 目标 / Swarm',
|
||||
goalLabel: '目标',
|
||||
swarmLabel: 'Swarm',
|
||||
modeOff: '未启用',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue