Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

76 lines
2.8 KiB
TypeScript

// Parses the durable plan→Issue reverse-link annotation the daemon writes
// after the user accepts a Tutti Mode plan.
//
// Wire contract (must stay in sync with ReportIssuePlanningLink in
// services/tuttid/service/agent/issue_planning_timeline.go):
//
// MessageID: "plan-issue:<issueID>"
// Kind: "session_audit", Role: "assistant"
// Payload.content: "[@<escaped title>](mention://workspace-issue/<issueID>?workspaceId=…)"
//
// Detection keys on the stable message identity (the messageId prefix) plus
// the exact single-mention markdown shape — never on prose — mirroring
// tuttiModeCheckpointWakeMarker. A plan-issue message whose body does not
// parse degrades to the regular assistant markdown rendering (still friendly,
// never the notice/error surface).
import { parseRichTextMentionHref } from "@tutti-os/ui-rich-text/core";
import type { AgentTuttiPlanIssueLinkVM } from "./contracts/agentMessageRowVM";
export const TUTTI_PLAN_ISSUE_LINK_MESSAGE_ID_PREFIX = "plan-issue:";
const WORKSPACE_ISSUE_MENTION_PROVIDER_ID = "workspace-issue";
export function isTuttiPlanIssueLinkMessageId(
messageId: string | null | undefined
): boolean {
const trimmed = messageId?.trim() ?? "";
return (
trimmed.startsWith(TUTTI_PLAN_ISSUE_LINK_MESSAGE_ID_PREFIX) &&
trimmed.length > TUTTI_PLAN_ISSUE_LINK_MESSAGE_ID_PREFIX.length
);
}
export function parseTuttiPlanIssueLink(
messageId: string | null | undefined,
body: string | null | undefined
): AgentTuttiPlanIssueLinkVM | null {
if (!isTuttiPlanIssueLinkMessageId(messageId)) {
return null;
}
const markdown = body?.trim() ?? "";
// The body must be exactly one markdown link. The daemon escapes []()\ in
// the label and percent-encodes the href, so "](" only occurs as the
// label/href separator.
if (!markdown.startsWith("[") || !markdown.endsWith(")")) {
return null;
}
const separatorIndex = markdown.lastIndexOf("](");
if (separatorIndex <= 0) {
return null;
}
const rawLabel = markdown.slice(1, separatorIndex);
const href = markdown.slice(separatorIndex + 2, -1).trim();
if (rawLabel.includes("](") || rawLabel.includes("\n") || !href) {
return null;
}
const mention = parseRichTextMentionHref(href, rawLabel);
if (
!mention ||
mention.providerId.trim().toLowerCase() !==
WORKSPACE_ISSUE_MENTION_PROVIDER_ID ||
!mention.entityId.trim()
) {
return null;
}
const issueId = mention.entityId.trim();
const title =
unescapeMarkdownLabel(rawLabel.replace(/^@+/u, "").trim()) || issueId;
return { issueId, title, mentionMarkdown: markdown };
}
/** Inverse of the daemon's label escaper (\\, \[, \], \(, \)). */
function unescapeMarkdownLabel(label: string): string {
return label.replace(/\\([\\[\]()])/gu, "$1");
}