✨ feat(vscode): 头脑风暴会话可清除,悬停提示改窄改紧凑
工单记着的头脑风暴会话 id 此前没有清除入口,续聊只能 resume 旧会话, 把几个版本前的提示词和它当时报的错一起带回来。会话 id 行新增清除按钮, 确认后把 state JSON 的 sessionId 置空,下次续聊新开会话;会话文件不动, 手上有 id 仍可 claude --resume 找回。 悬停气泡此前贴着视口右边测量尺寸:fixed 元素的可用宽度是「视口宽 - left」, 贴边测会被挤窄、行数变多,按那个高度定位就浮高一截。改成先在左上角量再落位, 字号行高收紧,提示文案压成一行。顺带修掉「重置为待办」的错误说明——它会停 会话、删 worktree 和未合并 PR,不是原样保留。 Claude-Session: https://claude.ai/code/session_01REdhpbaNyqWASF2HWsnTFv
This commit is contained in:
1 parent
9fadcac12b
commit
5bfd759822
11 files changed
+124
-23
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
||||
"publisher": "clurdra",
|
||||
"name": "superpowers-vscode-clurdra",
|
||||
"displayName": "Superpowers-clurdra",
|
||||
"version": "0.2.130",
|
||||
"version": "0.2.131",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"description": "Superpowers specs and plans Kanban explorer",
|
||||
"author": "clurdra",
|
||||
|
||||
@@ -538,6 +538,10 @@ export class KanbanWebviewPanel {
|
||||
void issues.handleUpdateBrainstormProfilePath(this, msg.issueNumber, msg.brainstormProfilePath)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'issue/clear-brainstorm-session') {
|
||||
void issues.handleClearBrainstormSession(this, msg.issueNumber)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'issue/update-test-profile-path') {
|
||||
void issues.handleUpdateTestProfilePath(this, msg.issueNumber, msg.testProfilePath)
|
||||
return
|
||||
|
||||
@@ -1976,6 +1976,77 @@ export async function handleUpdateProfilePath(panel: KanbanWebviewPanel, issueNu
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清掉工单记的头脑风暴会话 id,下次点 ▶ 续聊就新开一个会话。
|
||||
*
|
||||
* 用处:旧会话的历史被 `claude --resume` 原样带回来(里面可能还留着几个版本
|
||||
* 之前的提示词和它报的错),想从干净的上下文重新聊需求时用。会话文件本身不动,
|
||||
* 手上有 id 仍可用 `claude --resume <id>` 找回。
|
||||
*/
|
||||
export async function handleClearBrainstormSession(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
let previousValue: string | undefined
|
||||
try {
|
||||
const existingState = await panel.readIssueState(issueNumber)
|
||||
previousValue = typeof existingState.sessionId === 'string' ? existingState.sessionId : undefined
|
||||
}
|
||||
catch {
|
||||
previousValue = undefined
|
||||
}
|
||||
|
||||
if (!previousValue) {
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'info',
|
||||
message: `工单 #${issueNumber} 本来就没有头脑风暴会话`,
|
||||
dismissOnTimer: 4000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const choice = await window.showWarningMessage(
|
||||
`确定清除工单 #${issueNumber} 的头脑风暴会话 id(${previousValue})?下次续聊会新开一个会话,旧会话记录还在,可用 claude --resume 找回。`,
|
||||
{ modal: true },
|
||||
'清除',
|
||||
)
|
||||
if (choice !== '清除')
|
||||
return
|
||||
|
||||
try {
|
||||
await panel.mergeIssueState(issueNumber, { sessionId: '' })
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logger.add({
|
||||
level: 'error',
|
||||
source: 'panel',
|
||||
message: `清除 sessionId 失败 (issue #${issueNumber})`,
|
||||
details: message,
|
||||
})
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'error',
|
||||
message: `清除工单 #${issueNumber} 头脑风暴会话失败: ${message}`,
|
||||
dismissOnTimer: 6000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
panel.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber,
|
||||
patch: { sessionId: null },
|
||||
})
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'success',
|
||||
message: `工单 #${issueNumber} 头脑风暴会话已清除,下次续聊将新开会话`,
|
||||
dismissOnTimer: 5000,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a per-issue `brainstormProfilePath` override into the issue's state
|
||||
* JSON. Brainstorm start/resume use this profile (see resolveBrainstormProfilePath);
|
||||
|
||||
@@ -213,6 +213,7 @@ export type WebviewToExtension
|
||||
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
||||
| { type: 'issue/update-profile-path', issueNumber: number, profilePath: string }
|
||||
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
|
||||
| { type: 'issue/clear-brainstorm-session', issueNumber: number }
|
||||
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
|
||||
| { type: 'logs/fetch' }
|
||||
| { type: 'logs/clear' }
|
||||
|
||||
@@ -26,6 +26,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet<string> = new Set([
|
||||
'issue/update-auto-review',
|
||||
'issue/update-profile-path',
|
||||
'issue/update-brainstorm-profile-path',
|
||||
'issue/clear-brainstorm-session',
|
||||
'issue/update-test-profile-path',
|
||||
'dependency/set',
|
||||
'dependency/clear',
|
||||
|
||||
@@ -56,6 +56,7 @@ export function App() {
|
||||
closeIssue,
|
||||
closeSessionTab,
|
||||
continueBrainstorm,
|
||||
clearBrainstormSession,
|
||||
changeColumn,
|
||||
setDependency,
|
||||
clearDependency,
|
||||
@@ -388,6 +389,7 @@ export function App() {
|
||||
onResetToTodo={n => changeColumn(n, 'todo', selectedIssue?.source, selectedIssue?.externalId)}
|
||||
onCloseSessionTab={closeSessionTab}
|
||||
onContinueBrainstorm={continueBrainstorm}
|
||||
onClearBrainstormSession={clearBrainstormSession}
|
||||
onUpdateAutoReview={updateIssueAutoReview}
|
||||
onUpdateProfilePath={updateIssueProfilePath}
|
||||
onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath}
|
||||
|
||||
@@ -45,6 +45,7 @@ interface BottomTabsProps {
|
||||
onResetToTodo: (issueNumber: number) => void
|
||||
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
|
||||
onContinueBrainstorm: (issueNumber: number) => void
|
||||
onClearBrainstormSession: (issueNumber: number) => void
|
||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||
onUpdateProfilePath: (issueNumber: number, profilePath: string) => void
|
||||
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
||||
@@ -142,6 +143,7 @@ export function BottomTabs(props: BottomTabsProps) {
|
||||
onResetToTodo={props.onResetToTodo}
|
||||
onCloseSessionTab={props.onCloseSessionTab}
|
||||
onContinueBrainstorm={props.onContinueBrainstorm}
|
||||
onClearBrainstormSession={props.onClearBrainstormSession}
|
||||
onUpdateAutoReview={props.onUpdateAutoReview}
|
||||
onUpdateProfilePath={props.onUpdateProfilePath}
|
||||
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ClaudeProfile } from '../hooks/useIssues'
|
||||
import type { Issue, IssueColumn } from '../types'
|
||||
import type { PropertyGroup, RowAction } from './property-grid'
|
||||
import { ArrowRightLeft, CircleSlash, Download, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react'
|
||||
import { ArrowRightLeft, CircleSlash, Download, Eraser, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { isIssueLocked } from '../lib/dependencies'
|
||||
import { isIssueMine, issueOwnerLogin } from '../lib/issueOwnership'
|
||||
@@ -93,6 +93,7 @@ interface IssueDetailPanelProps {
|
||||
* 打进去,关了就 `claude --resume <id>` 带着提示词恢复,没会话就新开一个。
|
||||
*/
|
||||
onContinueBrainstorm: (issueNumber: number) => void
|
||||
onClearBrainstormSession: (issueNumber: number) => void
|
||||
/** Persist a per-issue `autoReview` override into the state JSON. */
|
||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||
/** 已知 Claude profile 列表,用于「配置文件」下拉选项。 */
|
||||
@@ -144,6 +145,7 @@ export function IssueDetailPanel({
|
||||
onResetToTodo,
|
||||
onCloseSessionTab,
|
||||
onContinueBrainstorm,
|
||||
onClearBrainstormSession,
|
||||
onUpdateAutoReview,
|
||||
profiles,
|
||||
onUpdateProfilePath,
|
||||
@@ -236,6 +238,16 @@ export function IssueDetailPanel({
|
||||
},
|
||||
})
|
||||
}
|
||||
if (hasSession) {
|
||||
secondaryActions.push({
|
||||
icon: <Eraser className="size-3.5" />,
|
||||
title: '清除会话:下次续聊新开一个,不再带旧会话的历史',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onClearBrainstormSession(issue.number)
|
||||
},
|
||||
})
|
||||
}
|
||||
return {
|
||||
key: 'sessionId',
|
||||
label: '头脑风暴会话id',
|
||||
@@ -523,7 +535,7 @@ export function IssueDetailPanel({
|
||||
],
|
||||
},
|
||||
],
|
||||
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles],
|
||||
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles],
|
||||
)
|
||||
|
||||
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
|
||||
@@ -617,7 +629,7 @@ export function IssueDetailPanel({
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
<Tooltip label={issue.source === 'youtrack' ? '在浏览器里打开这张 YouTrack 卡' : '在浏览器里打开这个 Gitea 工单'}>
|
||||
<Tooltip label="在浏览器里打开">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenInBrowser(issue.htmlUrl)}
|
||||
@@ -632,7 +644,7 @@ export function IssueDetailPanel({
|
||||
{issue.source !== 'youtrack' && !readOnly && (
|
||||
<>
|
||||
{issue.handoffAttachmentId && me !== undefined && (issue.assignees ?? []).includes(me) && (
|
||||
<Tooltip label={`接管 ${issue.handoffFrom ?? '?'} 移交过来的工单:拉回会话记录和 worktree`}>
|
||||
<Tooltip label={`接管 ${issue.handoffFrom ?? '?'} 的移交`}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isHandoffRunning(issue.number)}
|
||||
@@ -646,7 +658,7 @@ export function IssueDetailPanel({
|
||||
</Tooltip>
|
||||
)}
|
||||
{!issue.handoffAttachmentId && issue.column !== 'done' && ((issue.assignees ?? []).length === 0 || (me !== undefined && (issue.assignees ?? []).includes(me))) && (
|
||||
<Tooltip label="移交给同事:把会话记录打包传过去,对方接管后继续做">
|
||||
<Tooltip label="移交给同事">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isHandoffRunning(issue.number)}
|
||||
@@ -659,14 +671,14 @@ export function IssueDetailPanel({
|
||||
</Tooltip>
|
||||
)}
|
||||
{issue.handoffAttachmentId && !(me !== undefined && (issue.assignees ?? []).includes(me)) && (
|
||||
<Tooltip label={`${issue.handoffFrom ?? '?'} 已移交,等待 ${(issue.assignees ?? []).join(', ') || '?'} 接管`}>
|
||||
<Tooltip label={`${issue.handoffFrom ?? '?'} 已移交,等 ${(issue.assignees ?? []).join(', ') || '?'} 接管`}>
|
||||
<span className="inline-flex shrink-0 items-center rounded border border-[var(--vscode-panel-border)] px-1.5 py-0.5 text-[10px] opacity-70">
|
||||
待接管
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{(issue.column === 'in-progress' || issue.column === 'review') && (
|
||||
<Tooltip label="打回待办:卡片挪回待办列,会话和 worktree 不动">
|
||||
<Tooltip label="重置为待办(停会话、删 worktree 和未合并 PR)">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onResetToTodo(issue.number)}
|
||||
@@ -677,7 +689,7 @@ export function IssueDetailPanel({
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="关闭工单:Gitea 上置为 closed,卡片从看板消失,可在 Gitea 里重开">
|
||||
<Tooltip label="关闭工单(Gitea 里可重开)">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCloseIssue(issue.number)}
|
||||
@@ -687,7 +699,7 @@ export function IssueDetailPanel({
|
||||
<CircleSlash className="size-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label="删除工单:从 Gitea 彻底删掉,不可撤销">
|
||||
<Tooltip label="删除工单(不可撤销)">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => issue && onDeleteIssue?.(issue.number)}
|
||||
|
||||
@@ -25,14 +25,14 @@ interface TooltipProps {
|
||||
* 上方放不下就翻到下方,左右钳在视口内。
|
||||
*/
|
||||
export function Tooltip({ label, children, className }: TooltipProps): ReactElement {
|
||||
const [pos, setPos] = useState<{ top: number, left: number } | null>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
const wrapRef = useRef<HTMLSpanElement | null>(null)
|
||||
const bubbleRef = useRef<HTMLDivElement | null>(null)
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
|
||||
const hide = useCallback((): void => {
|
||||
clearTimeout(timerRef.current)
|
||||
setPos(null)
|
||||
setVisible(false)
|
||||
}, [])
|
||||
|
||||
const show = useCallback((): void => {
|
||||
@@ -40,18 +40,17 @@ export function Tooltip({ label, children, className }: TooltipProps): ReactElem
|
||||
return
|
||||
clearTimeout(timerRef.current)
|
||||
timerRef.current = setTimeout(() => {
|
||||
const rect = wrapRef.current?.getBoundingClientRect()
|
||||
if (rect)
|
||||
setPos({ top: rect.top, left: rect.left + rect.width / 2 })
|
||||
if (wrapRef.current)
|
||||
setVisible(true)
|
||||
}, SHOW_DELAY_MS)
|
||||
}, [label])
|
||||
|
||||
useEffect(() => () => clearTimeout(timerRef.current), [])
|
||||
|
||||
// 气泡渲染后才知道自身尺寸,这时才能定最终位置:默认在上方,
|
||||
// 上方空间不够就翻到触发元素下方;水平居中后钳进视口。
|
||||
// 气泡先在左上角量一次尺寸再落位。必须量在 left:0:fixed 元素的可用宽度是
|
||||
// 「视口宽 - left」,贴右边量会被挤窄、行数变多,按那个高度定位就会浮高一截。
|
||||
useEffect(() => {
|
||||
if (!pos)
|
||||
if (!visible)
|
||||
return
|
||||
const bubble = bubbleRef.current
|
||||
const rect = wrapRef.current?.getBoundingClientRect()
|
||||
@@ -65,7 +64,7 @@ export function Tooltip({ label, children, className }: TooltipProps): ReactElem
|
||||
bubble.style.top = `${top}px`
|
||||
bubble.style.left = `${left}px`
|
||||
bubble.style.visibility = 'visible'
|
||||
}, [pos])
|
||||
}, [visible])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -80,12 +79,12 @@ export function Tooltip({ label, children, className }: TooltipProps): ReactElem
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
{pos && label && createPortal(
|
||||
{visible && label && createPortal(
|
||||
<div
|
||||
ref={bubbleRef}
|
||||
role="tooltip"
|
||||
style={{ top: pos.top, left: pos.left, visibility: 'hidden' }}
|
||||
className="pointer-events-none fixed z-50 max-w-64 rounded border border-[var(--vscode-editorHoverWidget-border,var(--vscode-panel-border))] bg-[var(--vscode-editorHoverWidget-background,var(--vscode-editorWidget-background))] px-2 py-1 text-xs leading-snug text-[var(--vscode-editorHoverWidget-foreground,var(--vscode-foreground))] shadow-md"
|
||||
style={{ top: 0, left: 0, visibility: 'hidden' }}
|
||||
className="pointer-events-none fixed z-50 max-w-[22rem] rounded border border-[var(--vscode-editorHoverWidget-border,var(--vscode-panel-border))] bg-[var(--vscode-editorHoverWidget-background,var(--vscode-editorWidget-background))] px-2 py-1 text-[11px] leading-tight break-words text-[var(--vscode-editorHoverWidget-foreground,var(--vscode-foreground))] shadow-md"
|
||||
>
|
||||
{label}
|
||||
</div>,
|
||||
|
||||
@@ -230,6 +230,8 @@ export interface UseIssuesResult {
|
||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
||||
*/
|
||||
updateIssueBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
||||
/** 清掉工单记的头脑风暴会话 id,下次续聊新开会话。 */
|
||||
clearBrainstormSession: (issueNumber: number) => void
|
||||
/**
|
||||
* 覆盖该工单测试会话专用的 Claude 配置文件,乐观更新本地 state 后
|
||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
||||
@@ -652,6 +654,12 @@ export function useIssues(): UseIssuesResult {
|
||||
postMessage({ type: 'issue/update-brainstorm-profile-path', issueNumber, brainstormProfilePath })
|
||||
}, [])
|
||||
|
||||
// 清除头脑风暴会话 id:扩展侧先弹确认框,确认后才落库并推 issue/patch,
|
||||
// 所以这里不做乐观更新。
|
||||
const clearBrainstormSession = useCallback((issueNumber: number): void => {
|
||||
postMessage({ type: 'issue/clear-brainstorm-session', issueNumber })
|
||||
}, [])
|
||||
|
||||
const updateIssueTestProfilePath = useCallback((issueNumber: number, testProfilePath: string): void => {
|
||||
// Optimistically update local state so the dropdown reflects immediately
|
||||
// without remounting the issues list / detail panel via a full reload.
|
||||
@@ -930,5 +938,5 @@ export function useIssues(): UseIssuesResult {
|
||||
return cleanup
|
||||
}, [clearPrDiffSummaryRunning, markHandoffRunning])
|
||||
|
||||
return { state, settings, avatars, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
|
||||
return { state, settings, avatars, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearBrainstormSession, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
|
||||
}
|
||||
@@ -244,6 +244,7 @@ export type WebviewToExtension
|
||||
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
||||
| { type: 'issue/update-profile-path', issueNumber: number, profilePath: string }
|
||||
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
|
||||
| { type: 'issue/clear-brainstorm-session', issueNumber: number }
|
||||
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
|
||||
| { type: 'logs/fetch' }
|
||||
| { type: 'logs/clear' }
|
||||
|
||||
Reference in new issue
Block a user