fix(clean): correct project-tech field references and add sync vs clean disambiguation

- Fix Step 4.3: project.features → project.development_index,
  project.statistics → project._metadata
- Add project-guidelines.json learnings cleanup on session deletion
- Add sync vs clean disambiguation table explaining write vs reclaim
This commit is contained in:
catlog22
2026-02-25 22:34:15 +08:00
parent f96febe09a
commit c92754505a
9 changed files with 139 additions and 23 deletions

View File

@@ -475,21 +475,43 @@ if (selectedCategories.includes('Sessions')) {
}
}
// Update project-tech.json if features referenced deleted sessions
// Update project-tech.json: remove development_index entries referencing deleted sessions
const projectPath = '.workflow/project-tech.json'
if (fileExists(projectPath)) {
const project = JSON.parse(Read(projectPath))
const deletedPaths = new Set(results.deleted)
const deletedSessionIds = results.deleted
.filter(p => p.match(/WFS-|lite-plan/))
.map(p => p.split('/').pop())
project.features = project.features.filter(f =>
!deletedPaths.has(f.traceability?.archive_path)
)
project.statistics.total_features = project.features.length
project.statistics.last_updated = getUtc8ISOString()
if (project.development_index) {
for (const category of Object.keys(project.development_index)) {
project.development_index[category] = project.development_index[category].filter(entry =>
!deletedSessionIds.includes(entry.session_id)
)
}
}
project._metadata.last_updated = getUtc8ISOString()
Write(projectPath, JSON.stringify(project, null, 2))
}
// Update project-guidelines.json: remove learnings referencing deleted sessions
const guidelinesPath = '.workflow/project-guidelines.json'
if (fileExists(guidelinesPath)) {
const guidelines = JSON.parse(Read(guidelinesPath))
const deletedSessionIds = results.deleted
.filter(p => p.match(/WFS-|lite-plan/))
.map(p => p.split('/').pop())
if (guidelines.learnings) {
guidelines.learnings = guidelines.learnings.filter(l =>
!deletedSessionIds.includes(l.session_id)
)
}
guidelines._metadata.updated_at = getUtc8ISOString()
Write(guidelinesPath, JSON.stringify(guidelines, null, 2))
}
```
**Step 4.4: Report Results**
@@ -541,8 +563,10 @@ Cleanup manifest archived to: ${sessionFolder}/cleanup-manifest.json
| Manifest parse error | Regenerate from filesystem scan |
| Empty discovery | Report "codebase is clean" |
## Related Commands
- `/workflow:session:sync` - Sync session work to project-guidelines + project-tech (正向写入)
- `/workflow:session:complete` - Properly archive active sessions
- `memory-capture` skill - Save session memory before cleanup
- `workflow-execute` skill - View current workflow state

View File

@@ -75,20 +75,24 @@ function formatDuration(ms: number): string {
}
/**
* Get time ago string
* Hook to get localized time ago string
*/
function getTimeAgo(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
function useTimeAgo() {
const { formatMessage } = useIntl();
if (seconds < 60) return 'just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
return React.useCallback((dateString: string): string => {
const date = new Date(dateString);
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (seconds < 60) return formatMessage({ id: 'common.time.justNow' });
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return formatMessage({ id: 'common.time.minutesAgo' }, { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return formatMessage({ id: 'common.time.hoursAgo' }, { count: hours });
const days = Math.floor(hours / 24);
return formatMessage({ id: 'common.time.daysAgo' }, { count: days });
}, [formatMessage]);
}
/**
@@ -104,6 +108,7 @@ export function ConversationCard({
actionsDisabled = false,
}: ConversationCardProps) {
const { formatMessage } = useIntl();
const getTimeAgo = useTimeAgo();
const [copied, setCopied] = React.useState(false);
const status = statusConfig[execution.status] || statusConfig.error;
@@ -180,7 +185,7 @@ export function ConversationCard({
{execution.hasNativeSession && (
<Badge variant="outline" className="gap-1 text-xs">
<FileJson className="h-3 w-3" />
native
{formatMessage({ id: 'history.badge.native' })}
</Badge>
)}
<Badge variant={status.variant} className="gap-1 text-xs ml-auto">

View File

@@ -145,6 +145,7 @@ function TokenDisplay({ tokens, className }: TokenDisplayProps) {
* ToolCallItem - Single tool call display with collapsible details
*/
function ToolCallItem({ toolCall, index }: ToolCallItemProps) {
const { formatMessage } = useIntl();
return (
<details className="group/tool border border-border/50 rounded-md overflow-hidden">
<summary className="flex items-center gap-2 px-3 py-2 text-xs cursor-pointer hover:bg-muted/50 select-none">
@@ -155,7 +156,7 @@ function ToolCallItem({ toolCall, index }: ToolCallItemProps) {
<div className="border-t border-border/50 divide-y divide-border/50">
{toolCall.arguments && (
<div className="p-3">
<p className="text-xs font-medium text-muted-foreground mb-1">Input</p>
<p className="text-xs font-medium text-muted-foreground mb-1">{formatMessage({ id: 'nativeSession.toolCall.input' })}</p>
<pre className="p-2 bg-muted/30 rounded text-xs whitespace-pre-wrap overflow-x-auto font-mono leading-relaxed max-h-60 overflow-y-auto">
{toolCall.arguments}
</pre>
@@ -163,7 +164,7 @@ function ToolCallItem({ toolCall, index }: ToolCallItemProps) {
)}
{toolCall.output && (
<div className="p-3">
<p className="text-xs font-medium text-muted-foreground mb-1">Output</p>
<p className="text-xs font-medium text-muted-foreground mb-1">{formatMessage({ id: 'nativeSession.toolCall.output' })}</p>
<pre className="p-2 bg-muted/30 rounded text-xs whitespace-pre-wrap overflow-x-auto font-mono leading-relaxed max-h-60 overflow-y-auto">
{toolCall.output}
</pre>

View File

@@ -1,6 +1,10 @@
{
"title": "CLI Execution History",
"description": "View and manage your CLI execution history",
"tabs": {
"executions": "Executions",
"observability": "Session Audit"
},
"searchPlaceholder": "Search executions...",
"filterAllTools": "All Tools",
"deleteOptions": "Delete Options",
@@ -9,10 +13,14 @@
"deleteAll": "Delete All History",
"actions": {
"view": "View Details",
"viewNative": "View Native Session",
"delete": "Delete",
"copyId": "Copy ID",
"copied": "Copied!"
},
"badge": {
"native": "native"
},
"dialog": {
"deleteTitle": "Confirm Delete",
"deleteAllTitle": "Delete All History",

View File

@@ -41,6 +41,7 @@ import cliViewer from './cli-viewer.json';
import team from './team.json';
import terminalDashboard from './terminal-dashboard.json';
import skillHub from './skill-hub.json';
import nativeSession from './native-session.json';
/**
* Flattens nested JSON object to dot-separated keys
@@ -105,4 +106,5 @@ export default {
...flattenMessages(team, 'team'),
...flattenMessages(terminalDashboard, 'terminalDashboard'),
...flattenMessages(skillHub, 'skillHub'),
...flattenMessages(nativeSession, 'nativeSession'),
} as Record<string, string>;

View File

@@ -0,0 +1,33 @@
{
"title": "Native Session",
"tokens": {
"total": "Total tokens",
"input": "Input tokens",
"output": "Output tokens",
"cached": "Cached tokens"
},
"turn": {
"latest": "Latest",
"thoughts": "Thoughts",
"toolCalls": "Tool Calls"
},
"toolCall": {
"input": "Input",
"output": "Output"
},
"meta": {
"startTime": "Start time",
"workingDir": "Working directory",
"projectHash": "Project hash",
"turns": "turns"
},
"tokenSummary": "Total Tokens",
"loading": "Loading session...",
"error": "Failed to load session",
"empty": "No session data available",
"footer": {
"copied": "Copied!",
"copySessionId": "Copy Session ID",
"exportJson": "Export JSON"
}
}

View File

@@ -1,6 +1,10 @@
{
"title": "CLI 执行历史",
"description": "查看和管理 CLI 执行历史",
"tabs": {
"executions": "执行历史",
"observability": "会话审计"
},
"searchPlaceholder": "搜索执行记录...",
"filterAllTools": "全部工具",
"deleteOptions": "删除选项",
@@ -9,10 +13,14 @@
"deleteAll": "删除全部历史",
"actions": {
"view": "查看详情",
"viewNative": "查看原生会话",
"delete": "删除",
"copyId": "复制 ID",
"copied": "已复制!"
},
"badge": {
"native": "原生"
},
"dialog": {
"deleteTitle": "确认删除",
"deleteAllTitle": "删除全部历史",

View File

@@ -41,6 +41,7 @@ import cliViewer from './cli-viewer.json';
import team from './team.json';
import terminalDashboard from './terminal-dashboard.json';
import skillHub from './skill-hub.json';
import nativeSession from './native-session.json';
/**
* Flattens nested JSON object to dot-separated keys
@@ -105,4 +106,5 @@ export default {
...flattenMessages(team, 'team'),
...flattenMessages(terminalDashboard, 'terminalDashboard'),
...flattenMessages(skillHub, 'skillHub'),
...flattenMessages(nativeSession, 'nativeSession'),
} as Record<string, string>;

View File

@@ -0,0 +1,33 @@
{
"title": "原生会话",
"tokens": {
"total": "总 Token 数",
"input": "输入 Token",
"output": "输出 Token",
"cached": "缓存 Token"
},
"turn": {
"latest": "最新",
"thoughts": "思考过程",
"toolCalls": "工具调用"
},
"toolCall": {
"input": "输入",
"output": "输出"
},
"meta": {
"startTime": "开始时间",
"workingDir": "工作目录",
"projectHash": "项目哈希",
"turns": "轮次"
},
"tokenSummary": "总 Token",
"loading": "加载会话中...",
"error": "加载会话失败",
"empty": "无会话数据",
"footer": {
"copied": "已复制!",
"copySessionId": "复制会话 ID",
"exportJson": "导出 JSON"
}
}