|
@@ -3,7 +3,8 @@
|
|
|
<div class="codex-panel-head">
|
|
<div class="codex-panel-head">
|
|
|
<span>产物</span>
|
|
<span>产物</span>
|
|
|
<span class="codex-panel-head__count">
|
|
<span class="codex-panel-head__count">
|
|
|
- {{ filtered.length }}<template v-if="filtered.length !== entries.length"> / {{ entries.length }}</template>
|
|
|
|
|
|
|
+ {{ displayEntries.length
|
|
|
|
|
+ }}<template v-if="displayEntries.length !== displayTotal.length"> / {{ displayTotal.length }}</template>
|
|
|
</span>
|
|
</span>
|
|
|
<span class="codex-panel-head__spacer"></span>
|
|
<span class="codex-panel-head__spacer"></span>
|
|
|
<button class="codex-btn" type="button" :disabled="loading" @click="refresh()">
|
|
<button class="codex-btn" type="button" :disabled="loading" @click="refresh()">
|
|
@@ -16,6 +17,26 @@
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- 视图切换:默认看本会话产物,避免不同会话互相看到对方的文件 -->
|
|
|
|
|
+ <div v-if="root" class="codex-artifact__views">
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="codex-artifact__view"
|
|
|
|
|
+ :class="{ 'is-active': view === 'session' }"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ @click="switchView('session')"
|
|
|
|
|
+ >
|
|
|
|
|
+ 本会话产物
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="codex-artifact__view"
|
|
|
|
|
+ :class="{ 'is-active': view === 'all' }"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ @click="switchView('all')"
|
|
|
|
|
+ >
|
|
|
|
|
+ 全部文件
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<!-- 搜索 + 类型筛选:目录多了以后靠翻是翻不过来的 -->
|
|
<!-- 搜索 + 类型筛选:目录多了以后靠翻是翻不过来的 -->
|
|
|
<div v-if="root" class="codex-artifact__filters">
|
|
<div v-if="root" class="codex-artifact__filters">
|
|
|
<input v-model="keyword" class="codex-artifact__search" type="text" placeholder="按文件名筛选" />
|
|
<input v-model="keyword" class="codex-artifact__search" type="text" placeholder="按文件名筛选" />
|
|
@@ -23,7 +44,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 面包屑:只在进到子目录后出现 -->
|
|
<!-- 面包屑:只在进到子目录后出现 -->
|
|
|
- <div v-if="root" class="codex-artifact__crumb">
|
|
|
|
|
|
|
+ <div v-if="root && view === 'all'" class="codex-artifact__crumb">
|
|
|
<button v-if="relative" class="codex-btn" type="button" @click="onUp">
|
|
<button v-if="relative" class="codex-btn" type="button" @click="onUp">
|
|
|
<Icon icon="mdi:arrow-up" :size="13" />
|
|
<Icon icon="mdi:arrow-up" :size="13" />
|
|
|
上一级
|
|
上一级
|
|
@@ -64,20 +85,25 @@
|
|
|
新建会话后,Codex 生成的文件会出现在这里。
|
|
新建会话后,Codex 生成的文件会出现在这里。
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <template v-else-if="loading && !entries.length">
|
|
|
|
|
|
|
+ <template v-else-if="displayLoading && !displayEntries.length">
|
|
|
<div class="codex-skeleton codex-artifact__skeleton"></div>
|
|
<div class="codex-skeleton codex-artifact__skeleton"></div>
|
|
|
<div class="codex-skeleton codex-artifact__skeleton"></div>
|
|
<div class="codex-skeleton codex-artifact__skeleton"></div>
|
|
|
<div class="codex-skeleton codex-artifact__skeleton"></div>
|
|
<div class="codex-skeleton codex-artifact__skeleton"></div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
- <div v-else-if="!entries.length" class="codex-placeholder">
|
|
|
|
|
- 目录为空。让 Codex 生成图表或报告后,这里会出现产物。
|
|
|
|
|
|
|
+ <div v-else-if="!displayTotal.length" class="codex-placeholder">
|
|
|
|
|
+ <template v-if="view === 'session'">
|
|
|
|
|
+ 本会话还没有产生文件。<br />
|
|
|
|
|
+ 让 Codex 生成图表或报告后,它产出 / 修改的文件会出现在这里;<br />
|
|
|
|
|
+ 想看工作空间里的全部文件请切到「全部文件」。
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else> 目录为空。让 Codex 生成图表或报告后,这里会出现产物。 </template>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div v-else-if="!filtered.length" class="codex-placeholder">没有匹配的文件。</div>
|
|
|
|
|
|
|
+ <div v-else-if="!displayEntries.length" class="codex-placeholder">没有匹配的文件。</div>
|
|
|
|
|
|
|
|
<ul v-else class="codex-artifact__list">
|
|
<ul v-else class="codex-artifact__list">
|
|
|
- <li v-for="entry in sorted" :key="entry.relative" class="codex-artifact__item">
|
|
|
|
|
|
|
+ <li v-for="entry in displaySorted" :key="entry.relative" class="codex-artifact__item">
|
|
|
<button class="codex-artifact__main" type="button" :title="entry.relative" @click="onOpenEntry(entry)">
|
|
<button class="codex-artifact__main" type="button" :title="entry.relative" @click="onOpenEntry(entry)">
|
|
|
<Icon class="codex-artifact__icon" :icon="codexArtifactIcon(entry.kind)" :size="15" />
|
|
<Icon class="codex-artifact__icon" :icon="codexArtifactIcon(entry.kind)" :size="15" />
|
|
|
<span class="codex-artifact__name">{{ entry.name }}</span>
|
|
<span class="codex-artifact__name">{{ entry.name }}</span>
|
|
@@ -94,7 +120,7 @@
|
|
|
</li>
|
|
</li>
|
|
|
</ul>
|
|
</ul>
|
|
|
|
|
|
|
|
- <div v-if="truncated" class="codex-artifact__truncated">条目较多,仅显示前一部分。</div>
|
|
|
|
|
|
|
+ <div v-if="displayTruncated" class="codex-artifact__truncated">条目较多,仅显示前一部分。</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 预览 -->
|
|
<!-- 预览 -->
|
|
@@ -116,10 +142,14 @@
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
<script lang="ts" setup>
|
|
|
/**
|
|
/**
|
|
|
- * 产物栏:会话工作目录的文件浏览 / 预览 / 另存为 + 本轮回合的变更清单。
|
|
|
|
|
|
|
+ * 产物栏:双视图 + 预览 / 另存为 + 本轮回合的变更清单。
|
|
|
|
|
+ *
|
|
|
|
|
+ * - **本会话产物**(默认):工作空间里 mtime ≥ 会话创建时间的文件(含子目录)。
|
|
|
|
|
+ * 会话共用同一个案件工作空间,不做这份过滤的话每个会话看到的都是同一堆文件;
|
|
|
|
|
+ * - **全部文件**:按目录浏览工作空间(原有能力)。
|
|
|
*
|
|
*
|
|
|
* 目录信息全部来自主进程(沙箱限定在会话 cwd 内),
|
|
* 目录信息全部来自主进程(沙箱限定在会话 cwd 内),
|
|
|
- * 前端只保留「当前相对目录 + 筛选条件」这一点状态,刷新即回到主进程的真实结果。
|
|
|
|
|
|
|
+ * 前端只保留「当前视图 + 相对目录 + 筛选条件」这一点状态,刷新即回到主进程的真实结果。
|
|
|
*/
|
|
*/
|
|
|
import { computed, ref } from 'vue';
|
|
import { computed, ref } from 'vue';
|
|
|
import { Button, Modal, Select } from 'ant-design-vue';
|
|
import { Button, Modal, Select } from 'ant-design-vue';
|
|
@@ -157,6 +187,7 @@
|
|
|
const previewEntry = ref<CodexArtifactEntry | null>(null);
|
|
const previewEntry = ref<CodexArtifactEntry | null>(null);
|
|
|
const keyword = ref('');
|
|
const keyword = ref('');
|
|
|
const kindFilter = ref('all');
|
|
const kindFilter = ref('all');
|
|
|
|
|
+ const view = ref<'session' | 'all'>('session');
|
|
|
const expandedChange = ref('');
|
|
const expandedChange = ref('');
|
|
|
|
|
|
|
|
const root = computed(() => store.artifact.root);
|
|
const root = computed(() => store.artifact.root);
|
|
@@ -167,29 +198,48 @@
|
|
|
const changes = computed<CodexArtifactChange[]>(() => store.artifactChanges);
|
|
const changes = computed<CodexArtifactChange[]>(() => store.artifactChanges);
|
|
|
const newEntries = computed(() => store.artifact.newEntries);
|
|
const newEntries = computed(() => store.artifact.newEntries);
|
|
|
|
|
|
|
|
- /** 目录项永远显示(否则筛选后没法导航),文件按关键词 + 类型过滤 */
|
|
|
|
|
- const filtered = computed(() => {
|
|
|
|
|
|
|
+ const sessionEntries = computed(() => store.sessionFiles.entries);
|
|
|
|
|
+ const sessionLoading = computed(() => store.sessionFiles.loading);
|
|
|
|
|
+ const sessionTruncated = computed(() => store.sessionFiles.truncated);
|
|
|
|
|
+
|
|
|
|
|
+ /** 关键词 + 类型过滤;目录项永远显示(否则「全部文件」视图没法导航) */
|
|
|
|
|
+ function matchFilter(entry: CodexArtifactEntry): boolean {
|
|
|
const key = keyword.value.trim().toLowerCase();
|
|
const key = keyword.value.trim().toLowerCase();
|
|
|
|
|
+ if (entry.isDir) return true;
|
|
|
|
|
+ if (key && !entry.name.toLowerCase().includes(key)) return false;
|
|
|
const kind = kindFilter.value;
|
|
const kind = kindFilter.value;
|
|
|
- return entries.value.filter((entry) => {
|
|
|
|
|
- if (entry.isDir) return true;
|
|
|
|
|
- if (key && !entry.name.toLowerCase().includes(key)) return false;
|
|
|
|
|
- if (kind === 'all') return true;
|
|
|
|
|
- if (kind === 'table') return entry.kind === 'table';
|
|
|
|
|
- if (kind === 'image') return entry.kind === 'image';
|
|
|
|
|
- if (kind === 'report') return REPORT_KINDS.includes(entry.kind);
|
|
|
|
|
- if (kind === 'code') return CODE_KINDS.includes(entry.kind);
|
|
|
|
|
- return ![...REPORT_KINDS, ...CODE_KINDS, 'table', 'image'].includes(entry.kind);
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ if (kind === 'all') return true;
|
|
|
|
|
+ if (kind === 'table') return entry.kind === 'table';
|
|
|
|
|
+ if (kind === 'image') return entry.kind === 'image';
|
|
|
|
|
+ if (kind === 'report') return REPORT_KINDS.includes(entry.kind);
|
|
|
|
|
+ if (kind === 'code') return CODE_KINDS.includes(entry.kind);
|
|
|
|
|
+ return ![...REPORT_KINDS, ...CODE_KINDS, 'table', 'image'].includes(entry.kind);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- /** 目录在前,其次按修改时间倒序 */
|
|
|
|
|
- const sorted = computed(() =>
|
|
|
|
|
- [...filtered.value].sort((a, b) => {
|
|
|
|
|
|
|
+ const filtered = computed(() => entries.value.filter(matchFilter));
|
|
|
|
|
+ const filteredSession = computed(() => sessionEntries.value.filter(matchFilter));
|
|
|
|
|
+
|
|
|
|
|
+ /** 当前视图的取数 */
|
|
|
|
|
+ const displayTotal = computed(() => (view.value === 'all' ? entries.value : sessionEntries.value));
|
|
|
|
|
+ const displayEntries = computed(() => (view.value === 'all' ? filtered.value : filteredSession.value));
|
|
|
|
|
+ const displayLoading = computed(() => (view.value === 'all' ? loading.value : sessionLoading.value));
|
|
|
|
|
+ const displayTruncated = computed(() => (view.value === 'all' ? truncated.value : sessionTruncated.value));
|
|
|
|
|
+ /** 「全部文件」目录在前、按时间倒序;「本会话产物」主进程已按时间倒序 */
|
|
|
|
|
+ const displaySorted = computed(() => {
|
|
|
|
|
+ if (view.value !== 'all') return displayEntries.value;
|
|
|
|
|
+ return [...displayEntries.value].sort((a, b) => {
|
|
|
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
|
return Date.parse(b.mtime) - Date.parse(a.mtime);
|
|
return Date.parse(b.mtime) - Date.parse(a.mtime);
|
|
|
- }),
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ function switchView(next: 'session' | 'all') {
|
|
|
|
|
+ if (view.value === next) return;
|
|
|
|
|
+ view.value = next;
|
|
|
|
|
+ // 切视图时补一次数据:会话产物按需加载,「全部文件」回到上次浏览的目录
|
|
|
|
|
+ if (next === 'session') void store.loadSessionFiles();
|
|
|
|
|
+ else void refresh();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
const crumb = computed(() => relative.value || '会话工作目录');
|
|
const crumb = computed(() => relative.value || '会话工作目录');
|
|
|
|
|
|
|
@@ -209,6 +259,10 @@
|
|
|
|
|
|
|
|
async function refresh(dir?: string) {
|
|
async function refresh(dir?: string) {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ if (view.value === 'session') {
|
|
|
|
|
+ await store.loadSessionFiles();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
await store.refreshArtifacts(dir);
|
|
await store.refreshArtifacts(dir);
|
|
|
// 手动刷新后不再高亮
|
|
// 手动刷新后不再高亮
|
|
|
store.clearArtifactMarks();
|
|
store.clearArtifactMarks();
|
|
@@ -273,6 +327,37 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.codex-artifact {
|
|
.codex-artifact {
|
|
|
|
|
+ /* 视图切换:本会话产物(默认)/ 全部文件 */
|
|
|
|
|
+ &__views {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 2px;
|
|
|
|
|
+ flex: 0 0 auto;
|
|
|
|
|
+ padding: 6px 10px 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__view {
|
|
|
|
|
+ padding: 3px 10px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--codex-text-2);
|
|
|
|
|
+ background: none;
|
|
|
|
|
+ border: 1px solid transparent;
|
|
|
|
|
+ border-radius: var(--codex-radius-sm);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ color: var(--codex-text);
|
|
|
|
|
+ background: var(--codex-bg-hover);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.is-active {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--codex-primary);
|
|
|
|
|
+ background: var(--codex-primary-soft);
|
|
|
|
|
+ border-color: fade(@primary-color, 25%);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
&__filters {
|
|
&__filters {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|