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
39 changes: 38 additions & 1 deletion src/functions/todo-extract-langextract.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
When refreshAction metadata is provided, use it as advisory context about the
existing card. Prefer a clearer title/description suggested by cleanup
metadata only when the nearby source quote supports it.
The input may include taskChains. Treat each taskChain as the primary unit:
title should anchor to the user's original intent, while description should
anchor to the latest agent status, blocker, or next step. Low-information
user turns such as "继续", "重试", "再来一次", "retry", or "continue" must be
merged back into the previous related taskChain; do not create separate
cards for those turns.
Extract only UNRESOLVED, actionable items: explicit next actions, follow-ups,
failed validations, blocked work, and in-progress work that still needs doing.
DO NOT extract completed work, results, status reports, confirmations, or
Expand Down Expand Up @@ -137,11 +143,36 @@ def main() -> int:
return 0

lx = load_langextract()
text = "\n\n".join(
task_chains = payload.get("taskChains") or []
chain_texts = []
if isinstance(task_chains, list):
for chain in task_chains:
if not isinstance(chain, dict):
continue
user_id = chain.get("userObservationId", "")
status_id = chain.get("latestStatusObservationId", "")
user_intent = chain.get("userIntent", "")
latest_status = chain.get("latestStatus", "")
summary = chain.get("completionSummary", "")
next_step = chain.get("nextStep", "")
chain_texts.append(
"\n".join(
[
f"[taskChain:{chain.get('chainId', '')}]",
f"[obs:{user_id}] 用户意图: {user_intent}",
f"[obs:{status_id}] Agent最新状态: {latest_status}",
f"completionState: {chain.get('completionState', '')}",
f"completionSummary: {summary}",
f"nextStep: {next_step}",
]
)
)
block_text = "\n\n".join(
f"[obs:{b.get('sourceObservationId','')}]\n{b.get('text','')}"
for b in blocks
if isinstance(b, dict) and b.get("text")
)
text = "\n\n".join([part for part in chain_texts + [block_text] if part])
if not text.strip():
print(json.dumps({"todos": []}, ensure_ascii=False))
return 0
Expand Down Expand Up @@ -351,6 +382,12 @@ class DummyLx:
assert "做最后一次状态确认" in PROMPT
assert "Negative example" in PROMPT
assert "refreshAction metadata" in PROMPT
assert "taskChains" in PROMPT
assert "用户意图" in "\n".join([
f"[taskChain:test]",
f"[obs:u1] 用户意图: 继续",
f"[obs:a1] Agent最新状态: 下一步需要修复",
])
assert "0.55-0.81" in PROMPT
assert "dedupeKey must be a short STABLE slug" in PROMPT
assert "克隆 AI-Todo 仓库" in PROMPT
Expand Down
234 changes: 224 additions & 10 deletions src/functions/todo-extract.ts

Large diffs are not rendered by default.

95 changes: 90 additions & 5 deletions src/viewer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4259,6 +4259,8 @@ <h1>AI Todo</h1>
'act.section.earlierLead': 'Open work from the last 3-10 days is folded by default.',
'act.section.older': 'Older backlog',
'act.section.olderLead': 'These items are 10+ days old and may be stale.',
'act.section.legacy': 'Legacy extraction backlog',
'act.section.legacyLead': 'Cards from the old extraction path are hidden by default; search still finds them.',
'act.section.expand': 'Expand',
'act.section.collapse': 'Collapse',
'act.focus.label': 'Focus',
Expand Down Expand Up @@ -4485,6 +4487,8 @@ <h1>AI Todo</h1>
'act.section.earlierLead': '3-10 天内的开放事项默认折叠,按需展开核对。',
'act.section.older': '陈旧积压',
'act.section.olderLead': '这些事项已超过 10 天,可能已经过期。',
'act.section.legacy': '旧版抽取积压',
'act.section.legacyLead': '这些卡片来自旧抽取链路,默认隐藏;搜索时仍可找到。',
'act.section.expand': '展开',
'act.section.collapse': '收起',
'act.focus.label': '聚焦',
Expand Down Expand Up @@ -8850,6 +8854,8 @@ <h1>AI Todo</h1>
var frontier = (results[1] && (results[1].frontier || results[1].actions)) || [];

state.actions.items = explicitActions;
state.actions.todoExtract = results[0] && results[0].todoExtract;
syncTodoExtractStatus();
state.actions.reviewItems = [];
state.actions.frontier = frontier;
state.actions.loaded = true;
Expand Down Expand Up @@ -8889,12 +8895,55 @@ <h1>AI Todo</h1>
return !!result && (result.engine === 'langextract' || result.engine === 'mixed') && !result.llmFallback;
}

function clearTodoExtractStatusPoll() {
if (!state.actions.extractStatusPollTimer) return;
clearTimeout(state.actions.extractStatusPollTimer);
state.actions.extractStatusPollTimer = null;
}

function scheduleTodoExtractStatusPoll() {
if (state.actions.extractStatusPollTimer) return;
state.actions.extractStatusPollTimer = setTimeout(function() {
state.actions.extractStatusPollTimer = null;
if (!state.actions.extractInFlight) return;
refreshActionListsAfterExtract().then(function() {
if (state.activeTab === 'actions' && !actionsScrolledAway()) renderActions();
else if (state.activeTab === 'actions') state.actions.stale = true;
}).catch(function() {
if (state.actions.extractInFlight) scheduleTodoExtractStatusPoll();
});
}, 3000);
}

function syncTodoExtractStatus() {
var st = state.actions.todoExtract || {};
if (st.status === 'running') {
state.actions.extractInFlight = true;
state.actions.extractStatus = 'running';
state.actions.extractMessage = t('act.extract.background');
scheduleTodoExtractStatusPoll();
return;
}
clearTodoExtractStatusPoll();
if (state.actions.extractInFlight && st.status !== 'running') state.actions.extractInFlight = false;
if (st.status === 'done' && st.summary) {
state.actions.extractStatus = 'done';
state.actions.extractFallback = !todoExtractionUsedLlm(st.summary);
state.actions.extractMessage = todoExtractionSummary(st.summary);
} else if (st.status === 'error') {
state.actions.extractStatus = 'error';
state.actions.extractMessage = st.error || t('act.extract.failedExisting');
}
}

function refreshActionListsAfterExtract() {
return Promise.all([
apiGet('actions'),
apiGet('frontier')
]).then(function(results) {
state.actions.items = (results[0] && results[0].actions) || state.actions.items || [];
state.actions.todoExtract = results[0] && results[0].todoExtract;
syncTodoExtractStatus();
state.actions.frontier = (results[1] && (results[1].frontier || results[1].actions)) || state.actions.frontier || [];
state.actions.reviewItems = [];
return null;
Expand Down Expand Up @@ -9663,7 +9712,9 @@ <h1>AI Todo</h1>

if (search) {
items = items.filter(function(a) {
return (a.title + ' ' + (a.description || '') + ' ' + (a.tags || []).join(' ') + ' ' + (a.project || '')).toLowerCase().indexOf(search) >= 0;
var chain = (a && a.metadata && a.metadata.todoChain) || {};
return (a.title + ' ' + (a.description || '') + ' ' + (a.tags || []).join(' ') + ' ' + (a.project || '') + ' ' +
(chain.completionSummary || '') + ' ' + (chain.latestStatus || '') + ' ' + (chain.nextStep || '')).toLowerCase().indexOf(search) >= 0;
});
}
var metricItems = items.slice();
Expand Down Expand Up @@ -9702,6 +9753,16 @@ <h1>AI Todo</h1>
}
return s;
}
function actionChainStatusText(a) {
var chain = (a && a.metadata && a.metadata.todoChain) || null;
if (!chain || typeof chain !== 'object') return '';
var summary = String(chain.completionSummary || chain.latestStatus || chain.nextStep || '').trim();
if (!summary) return '';
summary = todoDisplayText(summary);
var state = String(chain.completionState || '').trim();
var prefix = state === 'completed' ? '✓ ' : (state === 'interrupted' ? '⏸ ' : '→ ');
return prefix + summary;
}
function todoDisplayText(text) {
return String(text || '')
.replace(/Needs attention|Needs your reply|Needs confirmation|Needs follow-up|In progress|Follow up|To confirm|Reply queue|Reply/gi, 'Todo')
Expand Down Expand Up @@ -9832,10 +9893,28 @@ <h1>AI Todo</h1>
function isOpenAction(a) {
return a && (a.status === 'active' || a.status === 'blocked' || a.status === 'pending');
}
function isNoisyChainOpenAction(a) {
var chain = (a && a.metadata && a.metadata.todoChain) || null;
if (!isOpenAction(a) || !chain) return false;
var statusText = String(chain.completionSummary || '') + ' ' + String(chain.latestStatus || '');
var text = statusText + ' ' + String(a.description || '');
var statusHasNextStep = /(下一步|仍需|还需|需要继续|需要|待处理|待确认|待跟进|blocked|阻塞|失败|卡住|error|failed)/i.test(statusText);
if (chain.completionState === 'completed') return true;
if (/<collaboration_mode>|#\s*Plan Mode\b|#\s*Agent Mode\b/i.test(text)) return true;
if (/已(?:完成|提交并推送|创建|通过|合并|上传|重启|新建)|服务已重启|正常|无需后续|no action needed|completed/i.test(statusText) && !statusHasNextStep) return true;
return false;
}
function isLegacyGeneratedOpenAction(a) {
return isOpenAction(a) && a.createdBy === 'todo-extract' && !(a.metadata && a.metadata.todoChain);
}
function splitDefaultOpenItems(list) {
var split = { focus: [], earlier: [], older: [] };
var split = { focus: [], earlier: [], older: [], legacy: [] };
(list || []).forEach(function(a) {
if (!isOpenAction(a)) return;
if (isLegacyGeneratedOpenAction(a) || isNoisyChainOpenAction(a)) {
split.legacy.push(a);
return;
}
var age = actionAgeDays(a);
if (age <= FOCUS_DAYS || (actionNeedsRecheck(a) && age <= STALE_DAYS)) split.focus.push(a);
else if (age > STALE_DAYS) split.older.push(a);
Expand Down Expand Up @@ -9867,7 +9946,7 @@ <h1>AI Todo</h1>
html += '<div class="action-priority-rail ' + priorityClass(a.priority) + '"></div>';
html += '<div class="action-candidate-main">';
html += '<div class="action-item-title">' + esc(compactActionTitle(a.title)) + '</div>';
var actionDesc = actionDescriptionText(a.description);
var actionDesc = actionChainStatusText(a) || actionDescriptionText(a.description);
if (actionDesc) html += '<div class="action-item-desc">' + esc(truncate(actionDesc, 120)) + '</div>';
if (actionNeedsRecheck(a)) html += '<div class="action-recheck-note">' + esc(t('act.recheck')) + '</div>';
// STEP-16 calm card: source + relative time are hidden at rest and fade in
Expand Down Expand Up @@ -9957,12 +10036,13 @@ <h1>AI Todo</h1>
// (statusFilter==='done')时则照常全列,不走折叠区。
// STEP-12:cancelled(归档/被更新丢弃/被合并)也不进默认活动视图——否则
// 合并/丢弃后卡片仍以「已取消」分组留在原处,看着像「没生效」。
if (defaultView) {
if (defaultView || (todoFilterActive && !search)) {
var defaultSplit = splitDefaultOpenItems(items);
html += renderTodoGroup(defaultSplit.focus);
html += renderFoldedOpenSection(defaultSplit.earlier, t('act.section.earlier'), t('act.section.earlierLead'), 'earlierOpenExpanded', 'toggle-earlier-open');
html += renderFoldedOpenSection(defaultSplit.older, t('act.section.older'), t('act.section.olderLead'), 'olderBacklogExpanded', 'toggle-older-backlog');
html += renderDoneTodaySection(items.filter(function(a) { return a.status === 'done'; }), frontierIds, renderActionCard);
html += renderFoldedOpenSection(defaultSplit.legacy, t('act.section.legacy'), t('act.section.legacyLead'), 'legacyBacklogExpanded', 'toggle-legacy-backlog');
if (defaultView) html += renderDoneTodaySection(items.filter(function(a) { return a.status === 'done'; }), frontierIds, renderActionCard);
} else {
if (statusFilter !== 'done') {
html += renderTodoGroup(items.filter(function(a) { return a.status === 'pending' || a.status === 'blocked' || a.status === 'active'; }));
Expand Down Expand Up @@ -10793,6 +10873,11 @@ <h1>AI Todo</h1>
renderActions();
return;
}
if (action === 'toggle-legacy-backlog') {
state.actions.legacyBacklogExpanded = !state.actions.legacyBacklogExpanded;
renderActions();
return;
}
if (action === 'toggle-briefings') {
state.inbox.briefingExpanded = !state.inbox.briefingExpanded;
renderActions();
Expand Down
4 changes: 4 additions & 0 deletions src/viewer/parts/app/05-i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
'act.section.earlierLead': 'Open work from the last 3-10 days is folded by default.',
'act.section.older': 'Older backlog',
'act.section.olderLead': 'These items are 10+ days old and may be stale.',
'act.section.legacy': 'Legacy extraction backlog',
'act.section.legacyLead': 'Cards from the old extraction path are hidden by default; search still finds them.',
'act.section.expand': 'Expand',
'act.section.collapse': 'Collapse',
'act.focus.label': 'Focus',
Expand Down Expand Up @@ -284,6 +286,8 @@
'act.section.earlierLead': '3-10 天内的开放事项默认折叠,按需展开核对。',
'act.section.older': '陈旧积压',
'act.section.olderLead': '这些事项已超过 10 天,可能已经过期。',
'act.section.legacy': '旧版抽取积压',
'act.section.legacyLead': '这些卡片来自旧抽取链路,默认隐藏;搜索时仍可找到。',
'act.section.expand': '展开',
'act.section.collapse': '收起',
'act.focus.label': '聚焦',
Expand Down
Loading
Loading