Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions src/web/server_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,18 @@ bool WebServer::Impl::session_model_deleted(const std::string& model_name) const
return true;
}

namespace {

void append_worktree_session(json& target, const WorktreeSessionInfo& worktree) {
if (!worktree.active()) return;
target["worktree"] = {
{"name", worktree.worktree_name},
{"branch", worktree.worktree_branch},
};
}

} // namespace

json WebServer::Impl::session_info_to_json(const SessionInfo& s, const SessionMeta* m) const {
json o;
const std::string model_name =
Expand Down Expand Up @@ -646,6 +658,7 @@ json WebServer::Impl::session_info_to_json(const SessionInfo& s, const SessionMe
token_usage_has_values(s.session_token_usage)
? s.session_token_usage
: (m ? m->session_token_usage : TokenUsage{}));
if (m) append_worktree_session(o, m->worktree);
if (m && !m->todos.empty()) {
o["todos"] = todo_items_to_json(m->todos);
o["todo_summary"] = todo_summary_to_json(m->todos);
Expand Down Expand Up @@ -684,14 +697,9 @@ json WebServer::Impl::session_meta_to_json(const SessionMeta& m, const std::stri
o["permission_mode"] = m.permission_mode.empty() ? "default" : m.permission_mode;
o["token_usage"] = token_usage_or_null(m.last_token_usage);
o["session_token_usage"] = token_usage_or_null(m.session_token_usage);
// 会话的 worktree 状态(add-webui-git-session-pill):前端 pill 在会话
// 开始后据此显示只读 worktree 徽标。inactive 时省略字段。
if (m.worktree.active()) {
o["worktree"] = {
{"name", m.worktree.worktree_name},
{"branch", m.worktree.worktree_branch},
};
}
// 会话的 worktree 状态(add-webui-git-session-pill):前端 pill 与侧栏
// 据此恢复只读状态。inactive 时省略字段。
append_worktree_session(o, m.worktree);
if (!m.todos.empty()) {
o["todos"] = todo_items_to_json(m.todos);
o["todo_summary"] = todo_summary_to_json(m.todos);
Expand Down
35 changes: 35 additions & 0 deletions tests/web/web_server_smoke_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,41 @@ TEST(WebServerHttp, CreateSessionThenListShowsActive) {
EXPECT_EQ(occurrences, 1) << "active 与 disk meta 必须合并为同一条";
}

// 场景:active session 走 SessionInfo 序列化,也必须保留持久化 meta 中的
// worktree 状态,否则侧栏刷新后当前会话会丢失 worktree 标识。
TEST(WebServerHttp, ActiveWorktreeSessionListIncludesWorktreeState) {
WebServerFixture fx;
auto post = cpr::Post(cpr::Url{fx.url("/api/sessions")},
cpr::Header{{"Content-Type", "application/json"}},
cpr::Body{R"({})"});
ASSERT_EQ(post.status_code, 201) << post.text;
const auto sid = json::parse(post.text)["session_id"].get<std::string>();

auto* entry = fx.registry->lookup(sid);
ASSERT_NE(entry, nullptr);
ASSERT_NE(entry->sm, nullptr);
EXPECT_EQ(entry->sm->ensure_active_session_id(), sid);

acecode::WorktreeSessionInfo worktree;
worktree.original_cwd = fx.cwd;
worktree.worktree_path = fx.cwd + "/.acecode/worktrees/ses-active-list";
worktree.worktree_name = "ses-active-list";
worktree.worktree_branch = "worktree-ses-active-list";
entry->sm->set_active_worktree(worktree);

auto list = cpr::Get(cpr::Url{fx.url("/api/sessions")});
ASSERT_EQ(list.status_code, 200) << list.text;
const auto sessions = json::parse(list.text);
const auto found = std::find_if(sessions.begin(), sessions.end(), [&](const auto& item) {
return item.value("id", std::string{}) == sid;
});
ASSERT_NE(found, sessions.end());
EXPECT_TRUE((*found)["active"].get<bool>());
ASSERT_TRUE(found->contains("worktree"));
EXPECT_EQ((*found)["worktree"]["name"], "ses-active-list");
EXPECT_EQ((*found)["worktree"]["branch"], "worktree-ses-active-list");
}

TEST(WebServerHttp, SessionUserMessageSearchFindsTextAndAttachmentNames) {
WebServerFixture fx;

Expand Down
3 changes: 3 additions & 0 deletions web/public/vs-icons/Worktree.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions web/src/components/ChatView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,15 @@ export function ChatView({ sessionRef, sessionId, onSessionPromoted, onCommandWo
await sendInputOrBuiltin(id, sendPayload);
if (worktreeIntent) {
setLocalWorktree({ sid: id, name: `ses-${id}` });
const noWorkspace = !!created?.target?.noWorkspace;
notifySessionListChanged({
reason: 'worktree-created',
sessionId: id,
workspaceHash: noWorkspace
? ''
: (created?.response?.workspace_hash || created?.target?.hash || ''),
noWorkspace,
});
}
}
if (payload.text.trim()) recordInputHistory(payload.text);
Expand Down Expand Up @@ -1703,6 +1712,15 @@ export function ChatView({ sessionRef, sessionId, onSessionPromoted, onCommandWo
.then(() => {
if (sessionWorktreeIntent) {
setLocalWorktree({ sid: targetSid, name: `ses-${targetSid}` });
const noWorkspace = !!(ref?.noWorkspace || ref?.no_workspace);
notifySessionListChanged({
reason: 'worktree-created',
sessionId: targetSid,
workspaceHash: noWorkspace
? ''
: (ref?.workspaceHash || ref?.workspace_hash || ''),
noWorkspace,
});
}
if (payload.text.trim()) recordInputHistory(payload.text);
if (!ref?.title) setTranscriptTitle(payload.text || composerAttachments[0]?.name || '附件消息');
Expand Down
4 changes: 3 additions & 1 deletion web/src/components/DesktopContextMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const ACTION_LABELS = {
[DESKTOP_CONTEXT_ACTIONS.COLLAPSE_DIRECTORY]: '折叠目录',
[DESKTOP_CONTEXT_ACTIONS.COPY_PREVIEW_TEXT]: '复制预览内容',
[DESKTOP_CONTEXT_ACTIONS.COPY_PREVIEW_METADATA]: '复制预览信息',
[DESKTOP_CONTEXT_ACTIONS.COPY_PREVIEW_IMAGE]: '复制图片',
[DESKTOP_CONTEXT_ACTIONS.COPY_FILE_DIFF]: '复制此文件 diff',
[DESKTOP_CONTEXT_ACTIONS.COPY_ALL_DIFFS]: '复制全部 diff',
[DESKTOP_CONTEXT_ACTIONS.LOCATE_IN_FILE_TREE]: '在文件树中定位',
Expand Down Expand Up @@ -182,7 +183,7 @@ async function copyTextWithToast(text, label = '已复制') {

export async function copyImageWithToast(target) {
const result = await copyImageToSystemClipboard(target?.copyImageUrl || target?.previewUrl || '', {
mimeType: target?.mimeType || '',
mimeType: target?.mimeType || target?.contentType || '',
});
if (result?.ok) {
toast({ kind: 'ok', text: '已复制图片' });
Expand Down Expand Up @@ -298,6 +299,7 @@ async function runAction(item, target, rememberedText = '', rememberedSelectionC
await copyTextWithToast(actionTarget?.url || actionTarget?.path || '');
break;
case DESKTOP_CONTEXT_ACTIONS.COPY_ATTACHMENT_IMAGE:
case DESKTOP_CONTEXT_ACTIONS.COPY_PREVIEW_IMAGE:
await copyImageWithToast(actionTarget);
break;
case DESKTOP_CONTEXT_ACTIONS.ADD_SELECTION_CONTEXT:
Expand Down
7 changes: 6 additions & 1 deletion web/src/components/FilePreviewContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export function FilePreviewContent({ api, cwd, path, wrapPreview, onToggleWrapPr
'data-desktop-preview-kind': state.kind || undefined,
'data-desktop-preview-size': Number.isFinite(state.size) ? String(state.size) : undefined,
'data-desktop-preview-content-type': state.contentType || undefined,
'data-desktop-preview-copy-image-url': state.kind === 'image' ? state.previewUrl || undefined : undefined,
};
if (state.kind === 'image') {
return (
Expand All @@ -218,7 +219,11 @@ export function FilePreviewContent({ api, cwd, path, wrapPreview, onToggleWrapPr
>
<img src={state.previewUrl} alt={path} draggable="false" />
</button>
<ImageLightbox preview={imagePreview} onClose={() => setImagePreview(null)} />
<ImageLightbox
preview={imagePreview}
contextMenuAttrs={previewAttrs}
onClose={() => setImagePreview(null)}
/>
</div>
);
}
Expand Down
1 change: 1 addition & 0 deletions web/src/components/Icon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const ICONS = {
warning: 'StatusWarning',
wordWrap: 'WordWrap',
world: 'World',
worktree: 'Worktree',
error: 'StatusError',
};

Expand Down
3 changes: 2 additions & 1 deletion web/src/components/ImageLightbox.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createPortal } from 'react-dom';
import { VsIcon } from './Icon.jsx';

export function ImageLightbox({ preview, onClose }) {
export function ImageLightbox({ preview, onClose, contextMenuAttrs }) {
if (!preview?.src) return null;

return createPortal(
Expand All @@ -24,6 +24,7 @@ export function ImageLightbox({ preview, onClose }) {
src={preview.src}
alt={preview.alt || ''}
className="max-w-full max-h-full object-contain shadow-xl"
{...contextMenuAttrs}
onClick={(event) => event.stopPropagation()}
/>
</div>,
Expand Down
115 changes: 63 additions & 52 deletions web/src/components/SettingsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const NAV = [
{ key: 'archived', label: '已归档会话', icon: 'archive' },
{ key: 'usage', label: '使用情况', icon: 'list' },
{ key: 'feedback', label: '问题反馈', icon: 'help' },
{ key: 'about', label: '关于', icon: 'info' },
];

const DEFAULT_UPGRADE_SERVICE_URL = 'http://2017studio.imwork.net:82/aupdate/';
Expand Down Expand Up @@ -196,7 +197,7 @@ export function SettingsPage({
onFontSizeChange={onFontSizeChange}
/>
)}
{activeNavKey === 'config' && <SectionConfig health={health} />}
{activeNavKey === 'config' && <SectionConfig />}
{activeNavKey === 'personalization' && <SectionPersonalization />}
{activeNavKey === 'skills' && <SectionSkills />}
{activeNavKey === 'mcp' && <SectionMCP />}
Expand All @@ -207,6 +208,7 @@ export function SettingsPage({
{activeNavKey === 'archived' && <SectionArchived />}
{activeNavKey === 'usage' && <SectionUsage />}
{activeNavKey === 'feedback' && <SectionFeedback />}
{activeNavKey === 'about' && <SectionAbout health={health} />}
</div>
</div>
</div>
Expand Down Expand Up @@ -462,16 +464,72 @@ function SectionAppearance({ theme, setTheme, fontSize, onFontSizeChange }) {
);
}

// ─── 配置 ──────────────────────────────────────────────────────────────────
// 真实接入:升级服务 URL。其余程序版本 / 依赖项 / 诊断 / 重置仍保留占位。
// ─── 关于 ──────────────────────────────────────────────────────────────────

function SectionConfig({ health }) {
function SectionAbout({ health }) {
const [webCoreInfo, setWebCoreInfo] = useState(null);

useEffect(() => {
let cancelled = false;
getCurrentWebCoreInfo()
.then((info) => {
if (!cancelled) setWebCoreInfo(info);
})
.catch(() => {
if (!cancelled) setWebCoreInfo(null);
});
return () => { cancelled = true; };
}, []);

const programVersionLabel = formatProgramVersion(health?.version);
const webCoreLabel = formatWebCoreLabel(webCoreInfo);
const webCoreDetail = formatWebCoreDetail(webCoreInfo);

return (
<>
<h2 className="text-xl font-bold mb-5">关于</h2>

{/* 程序版本 */}
<div className="flex items-center justify-between px-3.5 py-2.5 rounded-md bg-surface border border-border mb-2">
<div>
<div className="text-[13px] font-medium">当前版本</div>
<div className="text-[11px] text-fg-mute mt-0.5">ACECode 桌面 / TUI / Daemon 同版本号</div>
</div>
<span className="font-mono text-[12px] text-fg-2">{programVersionLabel}</span>
</div>

<div className="flex items-center justify-between gap-3 px-3.5 py-2.5 rounded-md bg-surface border border-border mb-2">
<div className="min-w-0">
<div className="text-[13px] font-medium">Web 核心</div>
<div className="text-[11px] text-fg-mute mt-0.5">当前桌面 WebView / 浏览器渲染核心</div>
</div>
<div className="min-w-0 max-w-[62%] text-right">
<div
className="font-mono text-[12px] text-fg-2 truncate"
title={webCoreLabel}
>
{webCoreLabel}
</div>
{webCoreDetail && (
<div
className="text-[10px] text-fg-mute truncate"
title={webCoreDetail}
>
{webCoreDetail}
</div>
)}
</div>
</div>
</>
);
}

function SectionConfig() {
const [upgradeUrl, setUpgradeUrl] = useState(DEFAULT_UPGRADE_SERVICE_URL);
const [upgradeLoading, setUpgradeLoading] = useState(true);
const [upgradeSaving, setUpgradeSaving] = useState(false);
const [upgradeSaved, setUpgradeSaved] = useState(false);
const [upgradeError, setUpgradeError] = useState('');
const [webCoreInfo, setWebCoreInfo] = useState(null);
const [depPython, setDepPython] = useState(true);
const [depNode, setDepNode] = useState(true);
const [depCsharp, setDepCsharp] = useState(false);
Expand All @@ -495,18 +553,6 @@ function SectionConfig({ health }) {
return () => { cancelled = true; };
}, []);

useEffect(() => {
let cancelled = false;
getCurrentWebCoreInfo()
.then((info) => {
if (!cancelled) setWebCoreInfo(info);
})
.catch(() => {
if (!cancelled) setWebCoreInfo(null);
});
return () => { cancelled = true; };
}, []);

const saveUpgradeUrl = async () => {
const baseUrl = upgradeUrl.trim();
if (!baseUrl || !/^https?:\/\//i.test(baseUrl)) {
Expand Down Expand Up @@ -551,9 +597,6 @@ function SectionConfig({ health }) {
{ key: 'node', label: 'Node.js 工具', desc: 'pnpm / npm / tsx 等', checked: depNode, toggle: () => setDepNode((v) => !v) },
{ key: 'csharp', label: 'C# 工具', desc: 'dotnet SDK / Roslyn 等', checked: depCsharp, toggle: () => setDepCsharp((v) => !v) },
];
const programVersionLabel = formatProgramVersion(health?.version);
const webCoreLabel = formatWebCoreLabel(webCoreInfo);
const webCoreDetail = formatWebCoreDetail(webCoreInfo);

return (
<>
Expand Down Expand Up @@ -627,38 +670,6 @@ function SectionConfig({ health }) {
<div className="text-[14px] font-semibold mb-1">工作空间依赖项</div>
<p className="text-[12px] text-fg-mute mb-3">管理 ACECode 安装并提供给 Agent 使用的开发工具</p>

{/* 程序版本 */}
<div className="flex items-center justify-between px-3.5 py-2.5 rounded-md bg-surface border border-border mb-2">
<div>
<div className="text-[13px] font-medium">当前版本</div>
<div className="text-[11px] text-fg-mute mt-0.5">ACECode 桌面 / TUI / Daemon 同版本号</div>
</div>
<span className="font-mono text-[12px] text-fg-2">{programVersionLabel}</span>
</div>

<div className="flex items-center justify-between gap-3 px-3.5 py-2.5 rounded-md bg-surface border border-border mb-2">
<div className="min-w-0">
<div className="text-[13px] font-medium">Web 核心</div>
<div className="text-[11px] text-fg-mute mt-0.5">当前桌面 WebView / 浏览器渲染核心</div>
</div>
<div className="min-w-0 max-w-[62%] text-right">
<div
className="font-mono text-[12px] text-fg-2 truncate"
title={webCoreLabel}
>
{webCoreLabel}
</div>
{webCoreDetail && (
<div
className="text-[10px] text-fg-mute truncate"
title={webCoreDetail}
>
{webCoreDetail}
</div>
)}
</div>
</div>

{/* 依赖项 checkbox 组 */}
<div className="rounded-md bg-surface border border-border px-3.5 py-3 mb-2">
<div className="text-[13px] font-medium mb-0.5">ACECode 依赖项</div>
Expand Down
Loading
Loading