Skip to content

Commit 704a07f

Browse files
王超claude
authored andcommitted
feat(chat): UltraPlan input tag + distinct menu/modal icons (v1.7.4)
- ui(chat): show a small inline blue "UltraPlan" tag in the user-bubble label row (right after the view-request button) when the input was an UltraPlan submission. Detection via new isUltraplanText() matching the <system-reminder>[SCOPED INSTRUCTION] marker anchored inside one reminder block, plus a message-level `ultraplan` flag from classifyUserContent(), threaded through both ChatView user-render branches into ChatMessage. - ui(menu): distinct icons for the log / prompt / messaging entries — 日志管理工具 -> FileTextOutlined, 查看用户 Prompt -> MessageOutlined, 通讯软件接入 + MessagingModal title -> new DialogueIcon (two-bubble dialogue). - ui(retry-config): hide the retry-config menu entry unless proxy mode is confirmed (change was already staged in the working tree). - test: add test/ultraplan-detection.test.js (isUltraplanText + classify flag). - chore: bump version 1.7.3 -> 1.7.4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ed90376 commit 704a07f

10 files changed

Lines changed: 174 additions & 13 deletions

File tree

history.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
- ui(chat): **`[UltraPlan]` tag above UltraPlan user bubbles** — when a chat user message was submitted via UltraPlan (`UltraPlanModal`/`buildLocalUltraplan`) or CLI `/ultraplan`, a small blue tag now appears above the user bubble so these special inputs stand out. Detection lives in the user-input classifier: new `isUltraplanText()` in `src/utils/contentFilter.js` matches the `<system-reminder>[SCOPED INSTRUCTION]…` marker (emitted only by `ultraplanTemplates.js`) on the **raw** text before `stripSystemTags` removes it — anchored inside a single reminder block (negative-lookahead on the close tag) to avoid tagging prose that merely mentions the phrase after an unrelated reminder. `classifyUserContent()` now returns a message-level `ultraplan` flag (additive key); `ChatView` threads it through both the array and string user-render branches (never on `plan-prompt`), and `ChatMessage.renderUserMessage()` renders the tag via new `.ultraplanTag`/`.ultraplanTagRow` classes (blue = `var(--color-primary)`, no `!important`). Reuses the existing `ui.ultraplan` label. New `test/ultraplan-detection.test.js` covers the detector (codeExpert/researchExpert/custom/seedPlan-prefixed + prose-mention / after-closed-reminder negatives) and the `classifyUserContent` flag (incl. non-first block via `.some`).
6+
- ui(menu): **distinct icons for the log/prompt/messaging entries** — the hamburger「日志管理工具」now uses a notepad (`FileTextOutlined`) and「查看用户 Prompt」a single speech bubble (`MessageOutlined`), replacing the near-identical import/export box-arrow glyphs. The「通讯软件接入」entry and the MessagingModal title use a new two-bubble dialogue icon (`src/components/common/DialogueIcon.jsx`, lucide *messages-square*, `.anticon`-wrapped `currentColor` stroke) so the two-party IM integration reads differently from the single-bubble prompt entry.
7+
8+
- ui(retry-config): **hide the「代理重试配置」menu entry unless proxy mode is confirmed** — the hamburger entry (and its pinned shortcut / Electron tab-bar pin, which all derive from the same menu descriptors) now only renders when a non-built-in proxy profile is active, or the built-in Default points at a non-official endpoint (reuses ProxyModal's `api.anthropic.com` origin test). On an official subscription — or before `/api/proxy-profiles` has loaded — the entry is hidden, since retry orchestration only targets proxy gateways.
9+
510
### Fix: `which claude` still shows the ccv wrapper after `ccv --uninstall` + `npm uninstall -g cc-viewer` (stale shell function → `command not found: ccv`)
611

712
- **Root cause:** the rc-file cleanup in `removeShellHook()` works — the marker block IS removed from disk — but every already-open shell keeps the `claude()` wrapper function loaded in memory (a child process cannot `unset -f` in the parent shell). Once `npm uninstall -g cc-viewer` deletes the `ccv` bin, the stale function's tail `ccv run -- claude --ccv-internal "$@"` hard-fails with `command not found: ccv`. The existing `unset -f claude` hint printed by `--uninstall` was one dim line immediately buried by the louder "done" message.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cc-viewer",
3-
"version": "1.7.3",
3+
"version": "1.7.4",
44
"description": "Claude Code logging, visualization, and management toolkit — launch a web viewer alongside Claude Code with full request/response tracing, proxy, and mobile support",
55
"license": "MIT",
66
"main": "server.js",

src/components/chat/ChatMessage.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ class ChatMessage extends React.Component {
187187
p.requestIndex !== n.requestIndex || p.cacheTotalTokens !== n.cacheTotalTokens || p.label !== n.label || p.isTeammate !== n.isTeammate ||
188188
p.animateAvatar !== n.animateAvatar ||
189189
p.isHistoryLog !== n.isHistoryLog ||
190+
p.isUltraplan !== n.isUltraplan ||
190191
p.userProfile !== n.userProfile || p.modelInfo !== n.modelInfo || p.imSenderMap !== n.imSenderMap || p.imAgent !== n.imAgent ||
191192
p.resultText !== n.resultText || p.toolName !== n.toolName ||
192193
p.onViewRequest !== n.onViewRequest || p.onOpenFile !== n.onOpenFile ||
@@ -1081,6 +1082,9 @@ class ChatMessage extends React.Component {
10811082
<div className={styles.labelRow}>
10821083
{timeStr && <Text className={styles.timeTextNoMargin}>{timeStr}</Text>}
10831084
{this.renderViewRequestBtn()}
1085+
{this.props.isUltraplan && (
1086+
<span className={styles.ultraplanTag}>{t('ui.ultraplan')}</span>
1087+
)}
10841088
{imBadge
10851089
? (
10861090
<span className={styles.imSourceNameGroup}>

src/components/chat/ChatMessage.module.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,22 @@
142142
margin-left: auto;
143143
}
144144

145+
/* Small blue "UltraPlan" tag shown inline in the label row (after the view-request
146+
button) for UltraPlan inputs. */
147+
.ultraplanTag {
148+
font-size: 10px;
149+
font-weight: 600;
150+
line-height: 1.4;
151+
padding: 0 6px;
152+
margin-left: 4px;
153+
border-radius: 4px;
154+
color: #fff;
155+
background: var(--color-primary);
156+
letter-spacing: 0.2px;
157+
white-space: nowrap;
158+
flex-shrink: 0;
159+
}
160+
145161
/* IM-source icon + username grouped together, kept right-aligned as a unit. */
146162
.imSourceNameGroup {
147163
display: inline-flex;

src/components/chat/ChatView.jsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { formatPromptNavTime } from '../../utils/formatters';
1515
import { buildPromptNavItems } from '../../utils/promptNav';
1616
import { getTeammateAvatar } from '../../utils/teammateAvatars';
1717
import { applyAvatarAnimationTargets } from '../../utils/avatarAnimationPostPass';
18-
import { isSystemText, classifyUserContent, isMainAgent, isTeammate, resolveTeammateNames, extractDisplayText } from '../../utils/contentFilter';
18+
import { isSystemText, classifyUserContent, isMainAgent, isTeammate, resolveTeammateNames, extractDisplayText, isUltraplanText } from '../../utils/contentFilter';
1919
import { classifyRequest, formatRequestTag, formatTeammateLabel } from '../../utils/requestType';
2020
import { playEvent as playVoiceEvent } from '../../utils/voicePackPlayer';
2121
import { buildChunksForAnswer, buildBracketPasteSubmitChunks, BRACKET_PASTE_SUBMIT_SETTLE_MS } from '../../utils/ptyChunkBuilder';
@@ -1341,7 +1341,7 @@ class ChatView extends React.Component {
13411341
if (suggestionText && toolResults.length > 0) {
13421342
// AskUserQuestion 的用户回复:跳过渲染(答案已在 assistant 侧问卷卡片上显示)
13431343
} else {
1344-
const { commands, textBlocks, skillBlocks, teammateBlocks, taskNotificationBlocks } = classifyUserContent(content);
1344+
const { commands, textBlocks, skillBlocks, teammateBlocks, taskNotificationBlocks, ultraplan } = classifyUserContent(content);
13451345
// 渲染 slash command 作为独立用户输入
13461346
for (let ci = 0; ci < commands.length; ci++) {
13471347
renderedMessages.push(
@@ -1360,7 +1360,7 @@ class ChatView extends React.Component {
13601360
for (let ti = 0; ti < textBlocks.length; ti++) {
13611361
const isPlan = /Implement the following plan:/i.test(textBlocks[ti].text || '');
13621362
renderedMessages.push(
1363-
<ChatMessage key={`${keyPrefix}-user-${mi}-${ti}`} role={isPlan ? 'plan-prompt' : 'user'} text={textBlocks[ti].text} lang={this.props.lang} timestamp={ts} userProfile={userProfile} modelInfo={modelInfo} requestIndex={hasViewRequest ? reqIdx : undefined} onViewRequest={hasViewRequest ? onViewRequest : undefined} isHistoryLog={isHistoryLog} />
1363+
<ChatMessage key={`${keyPrefix}-user-${mi}-${ti}`} role={isPlan ? 'plan-prompt' : 'user'} text={textBlocks[ti].text} isUltraplan={!isPlan && ultraplan} lang={this.props.lang} timestamp={ts} userProfile={userProfile} modelInfo={modelInfo} requestIndex={hasViewRequest ? reqIdx : undefined} onViewRequest={hasViewRequest ? onViewRequest : undefined} isHistoryLog={isHistoryLog} />
13641364
);
13651365
}
13661366
// 渲染 teammate-message 块
@@ -1420,8 +1420,9 @@ class ChatView extends React.Component {
14201420
const dispText = extractDisplayText(content);
14211421
if (dispText) {
14221422
const isPlan = /Implement the following plan:/i.test(dispText);
1423+
const ultra = !isPlan && isUltraplanText(content);
14231424
renderedMessages.push(
1424-
<ChatMessage key={`${keyPrefix}-user-${mi}`} role={isPlan ? 'plan-prompt' : 'user'} text={dispText} lang={this.props.lang} timestamp={ts} userProfile={userProfile} modelInfo={modelInfo} requestIndex={hasViewRequest ? reqIdx : undefined} onViewRequest={hasViewRequest ? onViewRequest : undefined} isHistoryLog={isHistoryLog} />
1425+
<ChatMessage key={`${keyPrefix}-user-${mi}`} role={isPlan ? 'plan-prompt' : 'user'} text={dispText} isUltraplan={ultra} lang={this.props.lang} timestamp={ts} userProfile={userProfile} modelInfo={modelInfo} requestIndex={hasViewRequest ? reqIdx : undefined} onViewRequest={hasViewRequest ? onViewRequest : undefined} isHistoryLog={isHistoryLog} />
14251426
);
14261427
}
14271428
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
3+
/**
4+
* Two-bubble "dialogue" icon (two facing conversation bubbles) for the
5+
* messaging (IM integration) menu entry. Distinguishes it from the single
6+
* bubble MessageOutlined used by the "view user prompts" entry.
7+
*
8+
* Wrapped in a `.anticon` span so it inherits the same 1em font-size sizing and
9+
* vertical alignment as the surrounding Ant Design menu icons; stroke uses
10+
* currentColor so it follows the active theme (same approach as OpenFolderIcon).
11+
*/
12+
export default function DialogueIcon({ style, className = '' }) {
13+
return (
14+
<span role="img" aria-label="dialogue" className={`anticon ${className}`.trim()} style={style}>
15+
<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
16+
<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z" />
17+
<path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1" />
18+
</svg>
19+
</span>
20+
);
21+
}

src/components/dashboard/AppHeader.jsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
33
import { Space, Tag, Button, Dropdown, Popover, Modal, Collapse, Drawer, Switch, Tabs, Spin, Input, Select, Segmented, Tooltip, message } from 'antd';
44
import { DISPLAY_SCALE_PRESETS } from '../../utils/displayScaleHelper';
55
import { hasNativeZoom, isMac } from '../../env';
6-
import { MessageOutlined, FileTextOutlined, ImportOutlined, DashboardOutlined, ExportOutlined, DownloadOutlined, SettingOutlined, BarChartOutlined, LineChartOutlined, CodeOutlined, CopyOutlined, ApiOutlined, SwapOutlined, EditOutlined, ThunderboltOutlined, QuestionCircleOutlined, PushpinOutlined, PushpinFilled } from '@ant-design/icons';
6+
import { MessageOutlined, FileTextOutlined, DashboardOutlined, DownloadOutlined, SettingOutlined, BarChartOutlined, LineChartOutlined, CodeOutlined, CopyOutlined, ApiOutlined, SwapOutlined, EditOutlined, ThunderboltOutlined, QuestionCircleOutlined, PushpinOutlined, PushpinFilled } from '@ant-design/icons';
77
import { QRCodeCanvas } from 'qrcode.react';
88
import { formatTokenCount, computeTokenStats, computeCacheRebuildStats, computeToolUsageStats, computeSkillUsageStats, readCalibrationModel, computeContextPercent, sumUsageInputTokens, sumUsageContextTokens } from '../../utils/helpers';
99
import { contextSeverityColor } from '../../utils/formatters';
@@ -23,6 +23,7 @@ import { SettingsContext } from '../../contexts/SettingsContext';
2323
import ConceptHelp from '../common/ConceptHelp';
2424
import ToolsHelp from '../common/ToolsHelp';
2525
import OpenFolderIcon from '../common/OpenFolderIcon';
26+
import DialogueIcon from '../common/DialogueIcon';
2627
import CachePopoverContent from './CachePopoverContent';
2728
import LiveTagPopover from './LiveTagPopover';
2829
import MemoryDetailModal from '../common/MemoryDetailModal';
@@ -151,17 +152,29 @@ class AppHeader extends React.Component {
151152

152153
// Electron 点击回传派发(_handleHeaderAction case 'menuShortcut')。
153154
// onClick 全部是 bound class-field arrow / inline arrow,可脱离菜单上下文 standalone 调用。
155+
// Retry config only applies when traffic goes through a proxy/third-party gateway.
156+
// "Confirmed proxy" = a non-built-in profile is active, or the built-in Default
157+
// points at a non-official endpoint (same api.anthropic.com test as ProxyModal's
158+
// Max warning). Official subscription (or config not yet loaded) → entry hidden.
159+
_isProxyMode() {
160+
const { activeProxyId, defaultConfig } = this.props;
161+
if (activeProxyId && activeProxyId !== 'max') return true;
162+
const origin = defaultConfig?.origin || '';
163+
return !!origin && !/api\.anthropic\.com/i.test(origin);
164+
}
165+
154166
_getMenuDescriptors() {
155167
const { viewMode, onImportLocalLogs, isLocalLog } = this.props;
156168
return [
157-
{ key: 'import-local', icon: <ImportOutlined />, label: t('ui.importLocalLogs'), onClick: onImportLocalLogs },
158-
{ key: 'export-prompts', icon: <ExportOutlined />, label: t('ui.exportPrompts'), onClick: this.handleShowPrompts },
169+
{ key: 'import-local', icon: <FileTextOutlined />, label: t('ui.importLocalLogs'), onClick: onImportLocalLogs },
170+
{ key: 'export-prompts', icon: <MessageOutlined />, label: t('ui.exportPrompts'), onClick: this.handleShowPrompts },
159171
{ key: 'plugin-management', icon: <ApiOutlined />, label: t('ui.pluginManagement'), onClick: this.handleShowPlugins },
160172
{ key: 'process-management', icon: <DashboardOutlined />, label: t('ui.processManagement'), onClick: this.handleShowProcesses },
161173
// 日志模式下 IM 无法正常配置/使用,隐藏 IM 配置入口
162-
...(isLocalLog ? [] : [{ key: 'messaging', icon: <MessageOutlined />, label: t('ui.messaging.menu'), onClick: () => this.setState({ messagingModalVisible: true, messagingInitialTool: null }) }]),
174+
...(isLocalLog ? [] : [{ key: 'messaging', icon: <DialogueIcon />, label: t('ui.messaging.menu'), onClick: () => this.setState({ messagingModalVisible: true, messagingInitialTool: null }) }]),
163175
{ key: 'proxy-switch', icon: <SwapOutlined />, label: t('ui.proxySwitch'), onClick: () => this.setState({ proxyModalVisible: true }) },
164-
{ key: 'retry-config', icon: <ThunderboltOutlined />, label: t('ui.retryConfig.title'), onClick: () => this.setState({ retryConfigModalVisible: true }) },
176+
// Hidden on official subscription: retry orchestration targets proxy gateways only
177+
...(this._isProxyMode() ? [{ key: 'retry-config', icon: <ThunderboltOutlined />, label: t('ui.retryConfig.title'), onClick: () => this.setState({ retryConfigModalVisible: true }) }] : []),
165178
{ key: 'edit-system-prompt', icon: <EditOutlined />, label: t('ui.expert.systemText'), onClick: () => this.setState({ systemTextModalVisible: true }), dividerAfter: true },
166179
{ key: 'project-stats', icon: <BarChartOutlined />, label: t('ui.projectStats'), onClick: this.handleShowProjectStats },
167180
...(viewMode === 'raw' ? [{ key: 'global-settings', icon: <SettingOutlined />, label: t('ui.globalSettings'), onClick: () => this.setState({ globalSettingsVisible: true }) }] : []),

src/components/settings/MessagingModal.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect } from 'react';
22
import { Modal } from 'antd';
3-
import { MessageOutlined } from '@ant-design/icons';
3+
import DialogueIcon from '../common/DialogueIcon';
44
import { imTr as _tr } from '../../utils/imTr';
55
import ImPlatformSettings from './ImPlatformSettings';
66
import { IM_PLATFORMS } from './imPlatforms';
@@ -37,7 +37,7 @@ export default function MessagingModal({ open, onClose, initialTool }) {
3737
// active tab "拉出贴合下方面板" 的 Chrome 标签观感在明暗主题都成立(对照 UltraPlan)。
3838
// header 同步取 --bg-elevated,否则 light 下标题栏(antd 默认 #FFF)会与 body(#F9F9F9)错色。
3939
styles={{ content: { background: 'var(--bg-elevated)' }, header: { background: 'var(--bg-elevated)' }, mask: BLUR_MASK_STYLE }}
40-
title={<span><MessageOutlined style={{ marginInlineEnd: 8 }} />{_tr('ui.messaging.title', null, 'Messaging Integrations')}</span>}
40+
title={<span><DialogueIcon style={{ marginInlineEnd: 8 }} />{_tr('ui.messaging.title', null, 'Messaging Integrations')}</span>}
4141
>
4242
<div className={styles.tabRow}>
4343
{TOOLS.map((tool) => {

src/utils/contentFilter.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,17 @@ export function isSyntheticPromptText(text) {
285285
return false;
286286
}
287287

288+
// UltraPlan 输入识别:UltraPlanModal / CLI /ultraplan 都会把 ultraplanTemplates.js
289+
// 组装的 <system-reminder>[SCOPED INSTRUCTION]…</system-reminder> 模板前置到用户 prompt。
290+
// 该 marker 仅由 ultraplanTemplates.js 产出,故可作为「本轮是 UltraPlan 输入」的可靠信号。
291+
// stripSystemTags 会剥掉该块,所以必须在原始(未剥离)文本上判断;用负向前瞻确保 marker
292+
// 出现在同一个 <system-reminder>…</system-reminder> 块内部(不跨过闭合标签),避免用户正文里
293+
// 在某个无关 reminder 之后只是「提到」该短语时误判。
294+
export function isUltraplanText(text) {
295+
if (!text || typeof text !== 'string') return false;
296+
return /<system-reminder>(?:(?!<\/system-reminder>)[\s\S])*?\[SCOPED INSTRUCTION\]/i.test(text);
297+
}
298+
288299
export function isSystemText(text) {
289300
if (!text) return true;
290301
const trimmed = text.trim();
@@ -469,7 +480,12 @@ export function classifyUserContent(content) {
469480
// 出现在 textBlocks 中;保留 skillBlocks 键以维持返回 shape(ChatView/ImConversationModal 消费)。
470481
const skillBlocks = [];
471482

472-
return { commands, textBlocks, skillBlocks, teammateBlocks, taskNotificationBlocks };
483+
// 本轮是否为 UltraPlan 输入(原始文本仍含 <system-reminder>[SCOPED INSTRUCTION] marker,
484+
// 此时尚未被 stripSystemTags 剥离)。消费方(ChatView)据此在用户气泡上方渲染 [UltraPlan] 标签。
485+
const ultraplan = Array.isArray(content)
486+
&& content.some(b => b && b.type === 'text' && isUltraplanText(b.text));
487+
488+
return { commands, textBlocks, skillBlocks, teammateBlocks, taskNotificationBlocks, ultraplan };
473489
}
474490

475491
/**

0 commit comments

Comments
 (0)