Przeglądaj źródła

refactor(codex): 移除会话内检索与结论快照功能

按用户要求下线 v2.0.4 的会话内检索(检索条 / 命中跳转 / 关键词高亮)与 v2.1.3 的结论快照(存快照 / 行级 diff / 快照导出)。删除 codexHighlight.ts、codexSnapshot.ts、codexTextDiff.ts、codexSnapshotPanel.vue 四个文件,恢复 markdown/代码块为纯渲染,移除工具栏两个入口与消息项命中态。产物栏文件名筛选、模板库搜索、关系图节点搜索是各自的过滤功能,不受影响。

验证:pnpm type:check 89 条基线 / 本包 0 报错;pnpm build exit=0。
cc 1 tydzień temu
rodzic
commit
396cba518e

+ 11 - 0
.workbuddy/memory/2026-09-24.md

@@ -208,3 +208,14 @@ HTML 报告里图表是折叠 option、未做行级 diff 染色、未真机 GUI
 **验证**:`pnpm type:check` 89 基线 / 本包 0 报错;`pnpm build` exit=0;
 主进程 `npx tsc --noEmit` 0 报错 + `npm test` 全过 + `npm run build-electron` exit=0。
 (产物服务没有单测覆盖 requireAllowedRoot —— 它依赖 Electron 的 dialog/shell。)
+
+## 移除「检索」与「快照」
+
+用户要求下线这两个功能:删 `codexMessageList` 检索条、`codexMessageItem` 命中态、
+markdown/代码块的关键词高亮透传、工具栏两个按钮,删除 4 个文件
+(`codexHighlight.ts` / `codexSnapshot.ts` / `codexTextDiff.ts` / `codexSnapshotPanel.vue`)。
+净删约 780 行;包 45 → 41 文件。
+保留产物栏文件名筛选、模板库搜索、关系图节点搜索(各自的过滤功能,与会话检索无关)。
+`listRef` 仍需要(PNG 长图导出要 `getCaptureElement`)。
+提交 `ba68d45`;连同前一条 `0a39a60`(工作目录代码决定 + 修拒绝访问)与用户自己的
+`6070d6f`,worktree 分支领先 dev_3/远端 3 个提交,**尚未合并推送**(用户未授权本次推送)。

+ 4 - 5
ai-electron/frontend/src/codex/components/codexBlockRenderer.vue

@@ -1,13 +1,12 @@
 <template>
   <div class="codex-blocks">
     <template v-for="block in blocks" :key="block.id">
-      <CodexMarkdownBlock v-if="block.type === 'text'" :content="block.content" :keyword="keyword" />
+      <CodexMarkdownBlock v-if="block.type === 'text'" :content="block.content" />
 
       <CodexCodeBlock
         v-else-if="block.type === 'code'"
         :content="block.content"
         :lang="block.lang"
-        :keyword="keyword"
       />
 
       <CodexJsonBlock v-else-if="block.type === 'json'" :content="block.content" />
@@ -35,7 +34,7 @@
           </div>
         </div>
         <div class="codex-card__body">
-          <CodexMarkdownBlock :content="block.content" :keyword="keyword" />
+          <CodexMarkdownBlock :content="block.content" />
         </div>
       </div>
     </template>
@@ -61,8 +60,8 @@
   import CodexGraphBlock from './codexGraphBlock.vue';
 
   const props = withDefaults(
-    defineProps<{ content: string; reportTitle?: string; keyword?: string }>(),
-    { reportTitle: 'codex', keyword: '' },
+    defineProps<{ content: string; reportTitle?: string }>(),
+    { reportTitle: 'codex' },
   );
 
   const { showMessage } = useMessage();

+ 0 - 12
ai-electron/frontend/src/codex/components/codexChatToolbar.vue

@@ -39,16 +39,6 @@
         重启
       </a-button>
 
-      <a-button size="small" :disabled="!hasMessages" @click="emit('search')">
-        <template #icon><Icon icon="mdi:magnify" :size="14" /></template>
-        检索
-      </a-button>
-
-      <a-button size="small" :disabled="!hasMessages" @click="emit('snapshot')">
-        <template #icon><Icon icon="mdi:content-save-outline" :size="14" /></template>
-        快照
-      </a-button>
-
       <a-dropdown :trigger="['click']" placement="bottomRight">
         <a-button size="small" :disabled="!hasMessages">
           <template #icon><Icon icon="mdi:download-outline" :size="14" /></template>
@@ -94,8 +84,6 @@
   const emit = defineEmits<{
     (e: 'create'): void;
     (e: 'diagnostics'): void;
-    (e: 'search'): void;
-    (e: 'snapshot'): void;
     (e: 'export', format: CodexReportFormat): void;
   }>();
 

+ 2 - 11
ai-electron/frontend/src/codex/components/codexCodeBlock.vue

@@ -25,11 +25,7 @@
       <div v-if="showLineNumbers" class="codex-pre__gutter">
         <div v-for="n in lineCount" :key="n">{{ n }}</div>
       </div>
-      <pre class="codex-pre__code"><code><span
-        v-for="(segment, index) in segments"
-        :key="index"
-        :class="{ 'codex-hl': segment.hit }"
-      >{{ segment.text }}</span></code></pre>
+      <pre class="codex-pre__code"><code>{{ content }}</code></pre>
     </div>
   </div>
 </template>
@@ -46,7 +42,6 @@
   import { downloadByData } from '@/core/utils/file/download';
   import { codexBlockLabel } from '../utils/codexBlockParser';
   import { copyCodexText, formatCodexSize, safeCodexFileName } from '../utils/codexFormat';
-  import { splitCodexHighlight } from '../utils/codexHighlight';
 
   const props = withDefaults(
     defineProps<{
@@ -55,10 +50,8 @@
       /** 折叠高度阈值(px),超过即默认折叠 */
       collapseHeight?: number;
       showLineNumbers?: boolean;
-      /** 会话内检索的关键词,命中处加高亮 */
-      keyword?: string;
     }>(),
-    { lang: '', collapseHeight: 480, showLineNumbers: true, keyword: '' },
+    { lang: '', collapseHeight: 480, showLineNumbers: true },
   );
 
   const { showMessage } = useMessage();
@@ -70,8 +63,6 @@
   const label = computed(() => codexBlockLabel('code', props.lang));
   const lineCount = computed(() => props.content.split('\n').length);
   const byteLength = computed(() => new Blob([props.content]).size);
-  /** 关键词命中切段(无关键词时就是单段原文) */
-  const segments = computed(() => splitCodexHighlight(props.content, props.keyword));
 
   const bodyStyle = computed(() =>
     collapsed.value ? { maxHeight: `${props.collapseHeight}px`, overflow: 'hidden' } : {},

+ 2 - 6
ai-electron/frontend/src/codex/components/codexMarkdownBlock.vue

@@ -9,15 +9,11 @@
    *
    * 渲染配置在 utils/codexMarkdown,html:false —— 不接受模型输出里的原始 HTML,
    * 需要富交互的内容一律走围栏块(图表 / 关系图 / 数据表)。
-   *
-   * keyword 非空时把命中关键词包成 <mark>:走字符串替换而不是改 DOM,
-   * 因为 v-html 每次更新是整块重建 innerHTML,字符串替换天然安全。
    */
   import { computed } from 'vue';
   import { renderCodexMarkdown } from '../utils/codexMarkdown';
-  import { highlightCodexHtml } from '../utils/codexHighlight';
 
-  const props = withDefaults(defineProps<{ content: string; keyword?: string }>(), { keyword: '' });
+  const props = defineProps<{ content: string }>();
 
-  const html = computed(() => highlightCodexHtml(renderCodexMarkdown(props.content), props.keyword));
+  const html = computed(() => renderCodexMarkdown(props.content));
 </script>

+ 4 - 33
ai-electron/frontend/src/codex/components/codexMessageItem.vue

@@ -1,17 +1,8 @@
 <template>
   <!-- 提问 -->
-  <div
-    v-if="message.role === 'user'"
-    class="codex-msg codex-msg--user"
-    :class="{ 'is-hit': hit }"
-    :data-msg-index="index"
-  >
+  <div v-if="message.role === 'user'" class="codex-msg codex-msg--user">
     <div class="codex-msg__bubble">
-      <div class="codex-msg__text"><span
-        v-for="(segment, index) in userSegments"
-        :key="index"
-        :class="{ 'codex-hl': segment.hit }"
-      >{{ segment.text }}</span></div>
+      <div class="codex-msg__text">{{ message.text }}</div>
       <div class="codex-msg__bar">
         <span class="codex-msg__time">{{ formatCodexTime(message.timestamp) }}</span>
         <span class="codex-msg__spacer"></span>
@@ -24,12 +15,7 @@
   </div>
 
   <!-- 回复 -->
-  <div
-    v-else
-    class="codex-msg codex-msg--assistant"
-    :class="{ 'is-hit': hit }"
-    :data-msg-index="index"
-  >
+  <div v-else class="codex-msg codex-msg--assistant">
     <div class="codex-msg__head">
       <Icon icon="mdi:robot-outline" :size="14" class="codex-msg__avatar" />
       <span class="codex-msg__role">Codex</span>
@@ -57,7 +43,6 @@
         v-if="message.content.trim()"
         :content="message.content"
         :report-title="reportTitle"
-        :keyword="keyword"
       />
 
       <div v-else-if="streaming" class="codex-msg__waiting">
@@ -89,7 +74,6 @@
   import type { CodexAssistantMessage, CodexChatMessage, CodexUserMessage } from '../types/codexTypes';
   import { copyCodexText, formatCodexTime } from '../utils/codexFormat';
   import { parseCodexBlocks } from '../utils/codexBlockParser';
-  import { splitCodexHighlight } from '../utils/codexHighlight';
   import { exportCodexTextMarkdown } from '../utils/codexReportExport';
   import { collectCodexTables } from '../utils/codexReportTemplate';
   import { utils, writeFile } from 'xlsx';
@@ -103,12 +87,8 @@
       index: number;
       reportTitle?: string;
       streaming?: boolean;
-      /** 命中消息流检索关键词时高亮(检索逻辑在 codexMessageList) */
-      hit?: boolean;
-      /** 检索关键词:正文里命中处再加文本级高亮 */
-      keyword?: string;
     }>(),
-    { reportTitle: 'codex', streaming: false, hit: false, keyword: '' },
+    { reportTitle: 'codex', streaming: false },
   );
 
   const emit = defineEmits<{
@@ -121,10 +101,6 @@
 
   const assistant = computed(() => props.message as CodexAssistantMessage);
 
-  /** 提问气泡的关键词切段(回复侧走 markdown/代码块各自的 keyword) */
-  const userSegments = computed(() =>
-    props.message.role === 'user' ? splitCodexHighlight(props.message.text, props.keyword) : [],
-  );
 
   const STATUS_LABELS: Record<string, string> = {
     running: '生成中',
@@ -183,11 +159,6 @@
       margin-top: var(--codex-sp-5);
     }
 
-    /* 命中检索:左侧加一条强调条(不做文本级染色,避免改动 markdown 渲染结果) */
-    &.is-hit {
-      padding-left: var(--codex-sp-3);
-      border-left: 2px solid fade(@primary-color, 55%);
-    }
 
     &__spacer {
       flex: 1 1 auto;

+ 2 - 128
ai-electron/frontend/src/codex/components/codexMessageList.vue

@@ -1,24 +1,5 @@
 <template>
   <div ref="scrollRef" class="codex-scroll" @scroll="onScroll">
-    <!-- 会话内检索:粘在滚动容器顶部,不影响消息流布局 -->
-    <div v-if="searchOpen" class="codex-search">
-      <Icon icon="mdi:magnify" :size="14" class="codex-search__icon" />
-      <input
-        ref="searchInputRef"
-        v-model="keyword"
-        class="codex-search__input"
-        type="text"
-        placeholder="在本次会话里检索:关键词"
-        @keydown.enter.prevent="step(1)"
-        @keydown.shift.enter.prevent="step(-1)"
-        @keydown.esc="closeSearch"
-      />
-      <span class="codex-search__count">{{ hitLabel }}</span>
-      <button class="codex-btn" type="button" :disabled="!hits.length" @click="step(-1)">上一条</button>
-      <button class="codex-btn" type="button" :disabled="!hits.length" @click="step(1)">下一条</button>
-      <button class="codex-btn" type="button" @click="closeSearch">关闭</button>
-    </div>
-
     <div ref="columnRef" class="codex-column codex-msg-list">
       <slot v-if="!messages.length" name="empty"></slot>
 
@@ -29,8 +10,6 @@
         :index="index"
         :report-title="reportTitle"
         :streaming="streaming && index === messages.length - 1"
-        :hit="hitSet.has(index)"
-        :keyword="activeKeyword"
         @regenerate="emit('regenerate', $event)"
         @resend="emit('resend', $event)"
       />
@@ -52,7 +31,7 @@
    * 检索:命中判定放在消息粒度(文本级染色会与 markdown 的 v-html 打架),
    * 命中项加左侧强调条,上/下一条用 scrollIntoView 居中定位。
    */
-  import { computed, nextTick, onMounted, ref, watch } from 'vue';
+  import { nextTick, onMounted, ref, watch } from 'vue';
   import Icon from '@/core/components/Icon/src/Icon.vue';
   import type { CodexChatMessage } from '../types/codexTypes';
   import CodexMessageItem from './codexMessageItem.vue';
@@ -69,42 +48,10 @@
 
   const scrollRef = ref<HTMLElement>();
   const columnRef = ref<HTMLElement>();
-  const searchInputRef = ref<HTMLInputElement>();
   const showToBottom = ref(false);
   /** 是否贴底:默认贴底,用户上翻后关闭 */
   let stickToBottom = true;
 
-  const searchOpen = ref(false);
-  const keyword = ref('');
-  /** 当前落在第几个命中(0 基) */
-  const cursor = ref(0);
-
-  /** 消息的可检索文本 */
-  function textOf(message: CodexChatMessage): string {
-    return message.role === 'user' ? message.text : message.content;
-  }
-
-  const hits = computed<number[]>(() => {
-    const key = keyword.value.trim().toLowerCase();
-    if (!key) return [];
-    const result: number[] = [];
-    props.messages.forEach((message, index) => {
-      if (textOf(message).toLowerCase().includes(key)) result.push(index);
-    });
-    return result;
-  });
-
-  const hitSet = computed(() => new Set(hits.value));
-
-  /** 检索条关闭时不传关键词,避免关掉检索后正文还留着高亮 */
-  const activeKeyword = computed(() => (searchOpen.value ? keyword.value.trim() : ''));
-
-  const hitLabel = computed(() => {
-    if (!keyword.value.trim()) return '—';
-    if (!hits.value.length) return '无命中';
-    return `${Math.min(cursor.value + 1, hits.value.length)} / ${hits.value.length}`;
-  });
-
   function onScroll() {
     const el = scrollRef.value;
     if (!el) return;
@@ -131,46 +78,8 @@
     () => toBottom(),
   );
 
-  /* ---------------- 检索 ---------------- */
-
-  function openSearch() {
-    searchOpen.value = true;
-    nextTick(() => searchInputRef.value?.focus());
-  }
-
-  function closeSearch() {
-    searchOpen.value = false;
-    keyword.value = '';
-    cursor.value = 0;
-  }
-
-  function toggleSearch() {
-    if (searchOpen.value) closeSearch();
-    else openSearch();
-  }
-
-  /** 跳到第 offset 个命中(相对当前) */
-  async function step(offset: number) {
-    const total = hits.value.length;
-    if (!total) return;
-    cursor.value = (cursor.value + offset + total) % total;
-    const index = hits.value[cursor.value];
-    await nextTick();
-    const el = columnRef.value?.querySelector<HTMLElement>(`[data-msg-index="${index}"]`);
-    if (!el) return;
-    // 检索期间关掉自动贴底,否则下一帧又被拽回底部
-    stickToBottom = false;
-    el.scrollIntoView({ block: 'center', behavior: 'smooth' });
-  }
-
-  // 关键词变化后从第一个命中开始
-  watch(keyword, () => {
-    cursor.value = 0;
-    if (keyword.value.trim()) nextTick(() => step(0));
-  });
-
   /** 长图导出用:返回内容列(不是滚动容器,滚动容器只有视口高度) */
-  defineExpose({ toBottom, getCaptureElement: () => columnRef.value, toggleSearch, closeSearch });
+  defineExpose({ toBottom, getCaptureElement: () => columnRef.value });
 </script>
 
 <style lang="less">
@@ -207,39 +116,4 @@
     }
   }
 
-  /* 会话内检索条 */
-  .codex-search {
-    position: sticky;
-    top: 0;
-    z-index: var(--codex-z-sticky);
-    display: flex;
-    align-items: center;
-    gap: var(--codex-sp-2);
-    margin: 0 var(--codex-sp-4);
-    padding: 6px var(--codex-sp-2);
-    background: var(--codex-bg);
-    border-bottom: 1px solid var(--codex-border);
-
-    &__icon {
-      flex: 0 0 auto;
-      color: var(--codex-text-2);
-    }
-
-    &__input {
-      flex: 1 1 auto;
-      min-width: 0;
-      height: 24px;
-      font-size: 12.5px;
-      color: var(--codex-text);
-      background: transparent;
-      border: 0;
-      outline: none;
-    }
-
-    &__count {
-      flex: 0 0 auto;
-      font-size: 11px;
-      color: var(--codex-text-3);
-    }
-  }
 </style>

+ 0 - 329
ai-electron/frontend/src/codex/components/codexSnapshotPanel.vue

@@ -1,329 +0,0 @@
-<template>
-  <a-drawer
-    :open="open"
-    title="结论快照"
-    wrap-class-name="codex-snapshot"
-    :width="760"
-    @close="emit('update:open', false)"
-  >
-    <div class="codex-snapshot__bar">
-      <a-input v-model:value="title" size="small" placeholder="快照标题(留空则用会话名 + 时间)" />
-      <a-button size="small" type="primary" :disabled="!hasMessages" @click="onSave">保存当前会话</a-button>
-    </div>
-    <p class="codex-snapshot__hint">
-      只存文本与表格,不存图表图片(图片会让单条快照上兆,localStorage 撑不住);单条上限 200KB、最多 20 条。
-    </p>
-
-    <div v-if="!snapshots.length" class="codex-placeholder">
-      还没有快照。分析出结论后存一份,隔几天再跑一次就能并排看差异。
-    </div>
-
-    <template v-else>
-      <div class="codex-snapshot__list">
-        <article v-for="snap in snapshots" :key="snap.id" class="codex-snapshot__item">
-          <a-checkbox :checked="selected.includes(snap.id)" @change="toggleSelect(snap.id)" />
-          <div class="codex-snapshot__meta">
-            <div class="codex-snapshot__title" :title="snap.title">{{ snap.title }}</div>
-            <div class="codex-snapshot__sub">
-              {{ formatCodexDateTime(snap.createdAt) }} · {{ sizeLabel(snap) }}<template v-if="snap.truncated">
-                · 已截断</template
-              >
-            </div>
-          </div>
-          <div class="codex-snapshot__actions">
-            <button class="codex-btn" type="button" @click="onExport(snap)">导出 MD</button>
-            <button class="codex-btn" type="button" @click="onRemove(snap.id)">删除</button>
-          </div>
-        </article>
-      </div>
-
-      <div v-if="diffResult" class="codex-snapshot__diff">
-        <div class="codex-snapshot__diff-head">
-          <Icon icon="mdi:compare-horizontal" :size="14" />
-          <span class="codex-snapshot__diff-title">
-            {{ nameOf(selected[0]) }} → {{ nameOf(selected[1]) }}
-          </span>
-          <span class="codex-snapshot__spacer"></span>
-          <button class="codex-btn" type="button" :class="{ 'is-on': collapseSame }" @click="collapseSame = !collapseSame">
-            折叠相同行
-          </button>
-        </div>
-        <div class="codex-snapshot__diff-body">
-          <div
-            v-for="(line, index) in diffLines"
-            :key="index"
-            class="codex-snapshot__diff-line"
-            :class="`is-${line.type}`"
-          >
-            <span class="codex-snapshot__sign">{{ SIGN[line.type] }}</span>
-            <span class="codex-snapshot__text">{{ line.text || ' ' }}</span>
-          </div>
-        </div>
-        <p v-if="diffResult.truncated" class="codex-snapshot__hint">内容过长,仅比较了前 1200 行。</p>
-      </div>
-      <p v-else-if="selected.length" class="codex-snapshot__hint">再勾一个快照即可对比(先勾的作为基准)。</p>
-    </template>
-  </a-drawer>
-</template>
-
-<script lang="ts" setup>
-  /**
-   * 结论快照抽屉:存快照 / 删快照 / 导出 / 并排 diff。
-   *
-   * 对比用**统一视图**(+/- 标记 + 行文本)而不是左右并排:
-   * 并排需要两列行号对齐,长报告里一列换行就会错位,统一视图不存在这个问题。
-   */
-  import { computed, ref, watch } from 'vue';
-  import { Button, Checkbox, Drawer, Input } from 'ant-design-vue';
-  import Icon from '@/core/components/Icon/src/Icon.vue';
-  import { useMessage } from '@/core/hooks/web/useMessage';
-  import { useCodexChatStore } from '../store/codexChatStore';
-  import type { CodexDiffLine, CodexSnapshot } from '../types/codexTypes';
-  import { formatCodexDateTime, formatCodexSize } from '../utils/codexFormat';
-  import { exportCodexTextMarkdown } from '../utils/codexReportExport';
-  import { codexConversationToMarkdown } from '../utils/codexReportTemplate';
-  import { collapseCodexDiff, diffCodexLines } from '../utils/codexTextDiff';
-  import {
-    readCodexSnapshots,
-    removeCodexSnapshot,
-    saveCodexSnapshot,
-  } from '../utils/codexSnapshot';
-
-  const props = defineProps<{ open: boolean }>();
-  const emit = defineEmits<{ (e: 'update:open', value: boolean): void }>();
-
-  const ADrawer = Drawer;
-  const AInput = Input;
-  const AButton = Button;
-  const ACheckbox = Checkbox;
-
-  const SIGN: Record<CodexDiffLine['type'], string> = { same: ' ', add: '+', del: '−' };
-
-  const store = useCodexChatStore();
-  const { showMessage } = useMessage();
-
-  const snapshots = ref<CodexSnapshot[]>(readCodexSnapshots());
-  /** 按勾选顺序保存 id:先勾的是基准 */
-  const selected = ref<string[]>([]);
-  const title = ref('');
-  const collapseSame = ref(true);
-
-  const hasMessages = computed(() => store.hasMessages);
-
-  const diffResult = computed(() => {
-    if (selected.value.length !== 2) return null;
-    const before = snapshots.value.find((item) => item.id === selected.value[0]);
-    const after = snapshots.value.find((item) => item.id === selected.value[1]);
-    if (!before || !after) return null;
-    return diffCodexLines(before.content, after.content);
-  });
-
-  const diffLines = computed<CodexDiffLine[]>(() => {
-    const result = diffResult.value;
-    if (!result) return [];
-    return collapseSame.value ? collapseCodexDiff(result.lines) : result.lines;
-  });
-
-  function refresh() {
-    snapshots.value = readCodexSnapshots();
-  }
-
-  // 每次打开都重读,避免外部改动导致列表过期
-  watch(
-    () => props.open,
-    (open) => {
-      if (!open) return;
-      refresh();
-      selected.value = [];
-      title.value = '';
-    },
-  );
-
-  function sizeLabel(snapshot: CodexSnapshot): string {
-    return formatCodexSize(new Blob([snapshot.content]).size);
-  }
-
-  function nameOf(id: string): string {
-    return snapshots.value.find((item) => item.id === id)?.title ?? '(已删除)';
-  }
-
-  function toggleSelect(id: string) {
-    const at = selected.value.indexOf(id);
-    if (at >= 0) {
-      selected.value = selected.value.filter((item) => item !== id);
-      return;
-    }
-    // 最多两个:再勾就顶掉最早的,避免出现「勾了三个不知道比哪两个」
-    selected.value = [...selected.value, id].slice(-2);
-  }
-
-  function onSave() {
-    const messages = store.messages;
-    if (!messages.length) {
-      showMessage('当前会话还没有内容', 'warning');
-      return;
-    }
-    const session = store.activeSession;
-    const content = codexConversationToMarkdown(messages, {
-      title: session?.title || 'Codex 会话',
-      cwd: store.activeCwd,
-      model: session?.model ?? null,
-      exportedAt: new Date().toLocaleString(),
-    });
-    const name = title.value.trim() || `${session?.title || '会话'} ${formatCodexDateTime(new Date())}`;
-    snapshots.value = saveCodexSnapshot({ title: name, content, cwd: store.activeCwd, model: session?.model ?? null });
-    title.value = '';
-    showMessage('已保存快照', 'success');
-  }
-
-  function onRemove(id: string) {
-    snapshots.value = removeCodexSnapshot(id);
-    selected.value = selected.value.filter((item) => item !== id);
-    showMessage('已删除快照', 'success');
-  }
-
-  function onExport(snapshot: CodexSnapshot) {
-    const name = exportCodexTextMarkdown(snapshot.title, snapshot.content);
-    showMessage(`已导出 ${name}`, 'success');
-  }
-</script>
-
-<style lang="less">
-  .codex-snapshot {
-    &__bar {
-      display: flex;
-      align-items: center;
-      gap: var(--codex-sp-2);
-      margin-bottom: var(--codex-sp-2);
-    }
-
-    &__hint {
-      margin: 0 0 var(--codex-sp-3);
-      font-size: 11.5px;
-      line-height: 1.7;
-      color: var(--codex-text-3);
-    }
-
-    &__spacer {
-      flex: 1 1 auto;
-    }
-
-    &__list {
-      display: flex;
-      flex-direction: column;
-      gap: 2px;
-      margin: 0 0 var(--codex-sp-3);
-      padding: 0;
-      list-style: none;
-    }
-
-    &__item {
-      display: flex;
-      align-items: center;
-      gap: var(--codex-sp-2);
-      padding: var(--codex-sp-2);
-      border: 1px solid var(--codex-border);
-      border-radius: var(--codex-radius-sm);
-
-      &:hover {
-        background: var(--codex-bg-hover);
-      }
-    }
-
-    &__meta {
-      flex: 1 1 auto;
-      min-width: 0;
-    }
-
-    &__title {
-      overflow: hidden;
-      font-size: 13px;
-      font-weight: 600;
-      color: var(--codex-text);
-      white-space: nowrap;
-      text-overflow: ellipsis;
-    }
-
-    &__sub {
-      margin-top: 1px;
-      font-size: 11px;
-      color: var(--codex-text-3);
-    }
-
-    &__actions {
-      display: flex;
-      align-items: center;
-      gap: 2px;
-      flex: 0 0 auto;
-    }
-
-    &__diff {
-      border: 1px solid var(--codex-border);
-      border-radius: var(--codex-radius-sm);
-      overflow: hidden;
-    }
-
-    &__diff-head {
-      display: flex;
-      align-items: center;
-      gap: 6px;
-      padding: 6px 8px;
-      font-size: 12px;
-      color: var(--codex-text-2);
-      background: var(--codex-bg-subtle);
-      border-bottom: 1px solid var(--codex-border);
-    }
-
-    &__diff-title {
-      min-width: 0;
-      overflow: hidden;
-      font-weight: 600;
-      color: var(--codex-text);
-      white-space: nowrap;
-      text-overflow: ellipsis;
-    }
-
-    &__diff-body {
-      max-height: 52vh;
-      overflow: auto;
-      font-family: 'SFMono-Regular', Consolas, monospace;
-      font-size: 11.5px;
-      line-height: 1.65;
-      background: var(--codex-bg);
-    }
-
-    &__diff-line {
-      display: flex;
-      gap: var(--codex-sp-2);
-      padding: 0 8px;
-      white-space: pre-wrap;
-      word-break: break-word;
-
-      &.is-add {
-        color: var(--codex-success);
-        background: fade(@success-color, 10%);
-      }
-
-      &.is-del {
-        color: var(--codex-error);
-        background: fade(@error-color, 10%);
-      }
-
-      &.is-same {
-        color: var(--codex-text-3);
-      }
-    }
-
-    &__sign {
-      flex: 0 0 auto;
-      width: 10px;
-      text-align: center;
-      user-select: none;
-    }
-
-    &__text {
-      flex: 1 1 auto;
-      min-width: 0;
-    }
-  }
-</style>

+ 0 - 8
ai-electron/frontend/src/codex/styles/codexTheme.less

@@ -647,14 +647,6 @@
   }
 }
 
-/* 会话内检索的关键词高亮(markdown 里的 <mark> 与插值切段共用同一套样式) */
-.codex-hl {
-  padding: 0 1px;
-  color: inherit;
-  background: fade(@warning-color, 34%);
-  border-radius: 2px;
-}
-
 /* 状态徽标 */
 .codex-badge {
   display: inline-flex;

+ 0 - 77
ai-electron/frontend/src/codex/utils/codexHighlight.ts

@@ -1,77 +0,0 @@
-/**
- * 会话内检索的关键词高亮。
- *
- * 两条路径,各自解决一类渲染方式:
- *  - `splitCodexHighlight`:给「用插值渲染」的地方(提问气泡、代码块)用,切成段落后在模板里加 class;
- *  - `highlightCodexHtml`:给「v-html 渲染」的 markdown 用,直接在 HTML 字符串上插 `<mark>`。
- *
- * 为什么不去改已渲染的 DOM:手工替换文本节点会破坏 Vue 缓存的 vnode 引用,
- * 下一次 patch 时文本会被写到已脱离文档的旧节点上,表现为「内容卡住不更新」。
- * 因此 markdown 走字符串替换(v-html 本来整块重建 innerHTML,天然安全),
- * 其它地方走插值切段,完全不碰渲染后的 DOM。
- */
-
-export interface CodexHighlightSegment {
-  text: string;
-  hit: boolean;
-}
-
-/** 把文本按关键词切成高亮段落(大小写不敏感);无关键词时原样返回单段 */
-export function splitCodexHighlight(text: string, keyword: string): CodexHighlightSegment[] {
-  const source = String(text ?? '');
-  const key = String(keyword ?? '').trim();
-  if (!source || !key) return [{ text: source, hit: false }];
-
-  const lowerKey = key.toLowerCase();
-  const lowerSource = source.toLowerCase();
-  const segments: CodexHighlightSegment[] = [];
-  let cursor = 0;
-
-  for (;;) {
-    const at = lowerSource.indexOf(lowerKey, cursor);
-    if (at < 0) break;
-    if (at > cursor) segments.push({ text: source.slice(cursor, at), hit: false });
-    segments.push({ text: source.slice(at, at + key.length), hit: true });
-    cursor = at + key.length;
-  }
-  if (cursor < source.length) segments.push({ text: source.slice(cursor), hit: false });
-
-  return segments.length ? segments : [{ text: source, hit: false }];
-}
-
-function escapeRegExp(value: string): string {
-  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
-
-/** 与 markdown-it 一致的转义范围:用于在「已转义文本」里匹配含特殊字符的关键词 */
-function escapeHtmlText(value: string): string {
-  return value
-    .replace(/&/g, '&amp;')
-    .replace(/</g, '&lt;')
-    .replace(/>/g, '&gt;')
-    .replace(/"/g, '&quot;');
-}
-
-/**
- * 在已渲染的 markdown HTML 里高亮关键词。
- *
- * 逐段扫描:`<...>` 标签原样跳过,只在标签之间的文本里替换,
- * 所以不会碰到标签名、属性值(例如 `<a href="...">` 里的链接地址)。
- *
- * 关键词会被 HTML 转义后再匹配(markdown-it 会把 `&` 输出成 `&amp;`),
- * 原始写法与转义写法各匹配一次,尽量不漏。
- */
-export function highlightCodexHtml(html: string, keyword: string): string {
-  const key = String(keyword ?? '').trim();
-  if (!html || !key) return html;
-
-  const escaped = escapeHtmlText(key);
-  const patterns = escaped === key ? [escapeRegExp(key)] : [escapeRegExp(key), escapeRegExp(escaped)];
-  const regex = new RegExp(patterns.join('|'), 'giu');
-
-  return html.replace(/(<[^>]+>)|([^<]+)/g, (_match, tag: string | undefined, text: string | undefined) => {
-    if (tag) return tag;
-    if (!text) return '';
-    return text.replace(regex, (hit: string) => `<mark class="codex-hl">${hit}</mark>`);
-  });
-}

+ 0 - 88
ai-electron/frontend/src/codex/utils/codexSnapshot.ts

@@ -1,88 +0,0 @@
-/**
- * 结论快照:把一次分析的结论(提问 + 回复正文 + 表格数据)存成本地快照,便于隔几天对比。
- *
- * 刻意**只存文本与表格、不存图片**:data URL 会让单条快照轻松上兆,
- * 而 localStorage 只有几 MB 配额,存图 = 两三条就把配额吃光。
- * 需要图就走 HTML / PNG 报告导出。
- */
-import type { CodexSnapshot } from '../types/codexTypes';
-import { codexTimestampTag } from './codexFormat';
-
-const SNAPSHOT_KEY = 'codex.snapshots.v1';
-/** 最多保留的快照条数 */
-const MAX_SNAPSHOTS = 20;
-/** 单条快照正文上限(200KB) */
-const MAX_CONTENT = 200 * 1024;
-
-function sanitize(list: unknown): CodexSnapshot[] {
-  if (!Array.isArray(list)) return [];
-  return list
-    .filter((item: any) => item && typeof item.id === 'string' && typeof item.content === 'string')
-    .map((item: any) => ({
-      id: item.id,
-      title: String(item.title ?? '未命名快照'),
-      createdAt: String(item.createdAt ?? new Date().toISOString()),
-      cwd: String(item.cwd ?? ''),
-      model: item.model ?? null,
-      content: item.content,
-      truncated: item.truncated === true,
-    }));
-}
-
-export function readCodexSnapshots(): CodexSnapshot[] {
-  try {
-    const raw = window.localStorage.getItem(SNAPSHOT_KEY);
-    if (!raw) return [];
-    return sanitize(JSON.parse(raw)).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
-  } catch {
-    return [];
-  }
-}
-
-function writeCodexSnapshots(list: CodexSnapshot[]): CodexSnapshot[] {
-  const sorted = list.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
-  const trimmed = sorted.slice(0, MAX_SNAPSHOTS);
-  try {
-    window.localStorage.setItem(SNAPSHOT_KEY, JSON.stringify(trimmed));
-  } catch {
-    // 配额不足:把最旧的一条挤掉再试一次,仍失败就放弃持久化(不打断用户)
-    const reduced = trimmed.slice(0, Math.max(1, trimmed.length - 1));
-    try {
-      window.localStorage.setItem(SNAPSHOT_KEY, JSON.stringify(reduced));
-      return reduced;
-    } catch {
-      return trimmed;
-    }
-  }
-  return trimmed;
-}
-
-/** 新增快照;内容超限时截断并标记 */
-export function saveCodexSnapshot(input: {
-  title: string;
-  content: string;
-  cwd?: string;
-  model?: string | null;
-}): CodexSnapshot[] {
-  const raw = String(input.content ?? '');
-  const truncated = raw.length > MAX_CONTENT;
-  const snapshot: CodexSnapshot = {
-    id: `snap-${codexTimestampTag()}-${Math.random().toString(36).slice(2, 6)}`,
-    title: String(input.title ?? '').trim().slice(0, 60) || '未命名快照',
-    createdAt: new Date().toISOString(),
-    cwd: String(input.cwd ?? ''),
-    model: input.model ?? null,
-    content: truncated ? `${raw.slice(0, MAX_CONTENT)}\n…(内容过长已截断)` : raw,
-    truncated,
-  };
-  return writeCodexSnapshots([snapshot, ...readCodexSnapshots()]);
-}
-
-export function removeCodexSnapshot(id: string): CodexSnapshot[] {
-  return writeCodexSnapshots(readCodexSnapshots().filter((item) => item.id !== id));
-}
-
-/** 快照占用是否接近配额(用于提示用户清理) */
-export function codexSnapshotLimits(): { max: number; maxContent: number } {
-  return { max: MAX_SNAPSHOTS, maxContent: MAX_CONTENT };
-}

+ 0 - 94
ai-electron/frontend/src/codex/utils/codexTextDiff.ts

@@ -1,94 +0,0 @@
-/**
- * 极简行级 diff(LCS 动态规划)。
- *
- * 用途单一:把两份结论快照并排看差异,不需要字符级 diff、不需要三方合并。
- * 因此不做 Myers 优化,直接用 DP —— 但必须限长:DP 是 O(n·m),
- * 行数一上去内存就爆(1200×1200 的 Int32 表约 5.8MB,已在安全范围)。
- */
-import type { CodexDiffLine } from '../types/codexTypes';
-
-/** 参与 diff 的最大行数(超出截断并给出提示) */
-const MAX_LINES = 1200;
-
-export interface CodexDiffResult {
-  lines: CodexDiffLine[];
-  /** 因超长被截断 */
-  truncated: boolean;
-}
-
-export function diffCodexLines(before: string, after: string): CodexDiffResult {
-  const left = String(before ?? '').split(/\r?\n/);
-  const right = String(after ?? '').split(/\r?\n/);
-
-  const truncated = left.length > MAX_LINES || right.length > MAX_LINES;
-  const a = truncated ? left.slice(0, MAX_LINES) : left;
-  const b = truncated ? right.slice(0, MAX_LINES) : right;
-  const n = a.length;
-  const m = b.length;
-
-  // dp[i][j] = a[i..] 与 b[j..] 的最长公共子序列长度
-  const width = m + 1;
-  const dp = new Int32Array((n + 1) * width);
-  for (let i = n - 1; i >= 0; i -= 1) {
-    for (let j = m - 1; j >= 0; j -= 1) {
-      dp[i * width + j] =
-        a[i] === b[j]
-          ? dp[(i + 1) * width + (j + 1)] + 1
-          : Math.max(dp[(i + 1) * width + j], dp[i * width + (j + 1)]);
-    }
-  }
-
-  const lines: CodexDiffLine[] = [];
-  let i = 0;
-  let j = 0;
-  while (i < n && j < m) {
-    if (a[i] === b[j]) {
-      lines.push({ type: 'same', text: a[i] });
-      i += 1;
-      j += 1;
-      continue;
-    }
-    // 优先走“删除”分支,保证输出稳定(同样的输入永远得到同样的结果)
-    if (dp[(i + 1) * width + j] >= dp[i * width + (j + 1)]) {
-      lines.push({ type: 'del', text: a[i] });
-      i += 1;
-    } else {
-      lines.push({ type: 'add', text: b[j] });
-      j += 1;
-    }
-  }
-  while (i < n) lines.push({ type: 'del', text: a[i++] });
-  while (j < m) lines.push({ type: 'add', text: b[j++] });
-
-  return { lines, truncated };
-}
-
-/**
- * 折叠上下文:只保留变更行前后 context 行,其余相同行合并成一条省略标记。
- * 长报告动辄几百行相同内容,不折叠的话差异会被淹掉。
- */
-export function collapseCodexDiff(lines: CodexDiffLine[], context = 3): CodexDiffLine[] {
-  const keep = new Array<boolean>(lines.length).fill(false);
-  lines.forEach((line, index) => {
-    if (line.type === 'same') return;
-    for (let offset = -context; offset <= context; offset += 1) {
-      const at = index + offset;
-      if (at >= 0 && at < lines.length) keep[at] = true;
-    }
-  });
-
-  const result: CodexDiffLine[] = [];
-  let skipping = false;
-  lines.forEach((line, index) => {
-    if (keep[index]) {
-      result.push(line);
-      skipping = false;
-      return;
-    }
-    if (!skipping) {
-      result.push({ type: 'same', text: '⋯' });
-      skipping = true;
-    }
-  });
-  return result;
-}

+ 0 - 9
ai-electron/frontend/src/codex/views/codexAnalysis/codexAnalysis.vue

@@ -13,8 +13,6 @@
       <CodexChatToolbar
         @create="onCreate"
         @diagnostics="diagnosticsOpen = true"
-        @search="onSearch"
-        @snapshot="snapshotOpen = true"
         @export="onExport"
       />
 
@@ -106,7 +104,6 @@
     <CodexApprovalDialog />
     <CodexDiagnosticDrawer v-model:open="diagnosticsOpen" />
     <CodexTemplatePanel v-model:open="templatesOpen" :draft="composerDraft" @pick="onPickTemplate" />
-    <CodexSnapshotPanel v-model:open="snapshotOpen" />
   </div>
 </template>
 
@@ -140,7 +137,6 @@
   import CodexDiagnosticDrawer from '../../components/codexDiagnosticDrawer.vue';
   import CodexTemplatePanel from '../../components/codexTemplatePanel.vue';
   import CodexBoardPanel from '../../components/codexBoardPanel.vue';
-  import CodexSnapshotPanel from '../../components/codexSnapshotPanel.vue';
 
   const AButton = Button;
 
@@ -152,7 +148,6 @@
   const listRef = ref<InstanceType<typeof CodexMessageList>>();
   const diagnosticsOpen = ref(false);
   const templatesOpen = ref(false);
-  const snapshotOpen = ref(false);
   const starting = ref(false);
   /** 输入框草稿:模板抽屉要拿它做「存为模板」 */
   const composerDraft = ref('');
@@ -273,10 +268,6 @@
     composerRef.value?.fill(prompt);
   }
 
-  function onSearch() {
-    listRef.value?.toggleSearch();
-  }
-
   async function onCreate() {
     await nextTick();
     listRef.value?.toBottom(true);

+ 7 - 0
codex-analysis-plan.md

@@ -551,3 +551,10 @@ dist/assets/app-antd-dark-theme-style.e3b0c442.css
 - **是否本次引入**:不是。`--ai-*` 在 v2.1 之前就是这个状态,本次只是把 codex 模块也纳入了同一套机制。
 - **修与不修**:修它要动构建期的主题插件(`build/theme/*` + `vite-plugin-theme-vite3` 的产物处理),
   属于构建管线改造,**风险与收益不匹配**,因此本次只记录、不动手;需要时单独立项。
+
+### 9.5 变更记录
+
+| 时间 | 变更 |
+| --- | --- |
+| 2026-09-24 晚 | **工作目录改为纯代码决定、界面不可改**:删除 `codexWorkspacePicker` 与 store 的 `cwdOverrides` / `recentCwds` / `setWorkspaceDir` 等,cwd 优先级简化为「会话 cwd → 案件工作空间 → 客户端默认」,界面只读展示;同时修复产物栏「该目录不是本客户端的会话工作目录,已拒绝访问」(`requireAllowedRoot` 对绝对路径且真实存在的目录自动补登记,解决「还没有会话就列产物」的时序问题) |
+| 2026-09-24 晚 | **移除「检索」与「快照」两个功能**(用户要求):删除 `codexMessageList` 的检索条、`codexMessageItem` 的命中态、`codexMarkdownBlock` / `codexCodeBlock` 的关键词高亮透传、工具栏两个按钮,以及 `utils/codexHighlight.ts`、`utils/codexSnapshot.ts`、`utils/codexTextDiff.ts`、`components/codexSnapshotPanel.vue` 四个文件。v2.0.4 的「会话内检索」与 v2.1.3 的「结论快照」就此下线;保留产物栏自己的文件名筛选、模板库搜索、关系图节点搜索(这些是各自的过滤功能,与被移除的会话检索无关)。包体积 45 → 41 文件 |