@pencil-agent/nano-pencil 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -82,8 +82,11 @@ export declare class ExtensionRunner {
82
82
  private shortcutDiagnostics;
83
83
  private commandDiagnostics;
84
84
  private readonly beforeAgentStartTimeoutMs;
85
+ private readonly beforeAgentStartTimeoutSentinel;
86
+ private beforeAgentStartTimeoutLastReported;
85
87
  constructor(extensions: Extension[], runtime: ExtensionRuntime, cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry);
86
88
  private withTimeout;
89
+ private reportBeforeAgentStartTimeout;
87
90
  bindCore(actions: ExtensionActions, contextActions: ExtensionContextActions): void;
88
91
  bindCommandContext(actions?: ExtensionCommandContextActions): void;
89
92
  setUIContext(uiContext?: ExtensionUIContext): void;
@@ -105,6 +105,8 @@ export class ExtensionRunner {
105
105
  shortcutDiagnostics = [];
106
106
  commandDiagnostics = [];
107
107
  beforeAgentStartTimeoutMs = 1500;
108
+ beforeAgentStartTimeoutSentinel = Symbol("before_agent_start_timeout");
109
+ beforeAgentStartTimeoutLastReported = new Map();
108
110
  constructor(extensions, runtime, cwd, sessionManager, modelRegistry) {
109
111
  this.extensions = extensions;
110
112
  this.runtime = runtime;
@@ -115,7 +117,7 @@ export class ExtensionRunner {
115
117
  }
116
118
  async withTimeout(promise, timeoutMs) {
117
119
  return new Promise((resolve) => {
118
- const timer = setTimeout(() => resolve(undefined), timeoutMs);
120
+ const timer = setTimeout(() => resolve(this.beforeAgentStartTimeoutSentinel), timeoutMs);
119
121
  promise
120
122
  .then((value) => {
121
123
  clearTimeout(timer);
@@ -123,10 +125,20 @@ export class ExtensionRunner {
123
125
  })
124
126
  .catch(() => {
125
127
  clearTimeout(timer);
126
- resolve(undefined);
128
+ resolve(this.beforeAgentStartTimeoutSentinel);
127
129
  });
128
130
  });
129
131
  }
132
+ reportBeforeAgentStartTimeout(extensionPath) {
133
+ const now = Date.now();
134
+ const last = this.beforeAgentStartTimeoutLastReported.get(extensionPath) ?? 0;
135
+ // Rate-limit to once per minute per extension.
136
+ if (now - last < 60_000)
137
+ return;
138
+ this.beforeAgentStartTimeoutLastReported.set(extensionPath, now);
139
+ // Soft warning only; do not surface as user-facing extension error.
140
+ console.warn(`Extension before_agent_start timed out (${this.beforeAgentStartTimeoutMs}ms): ${extensionPath}`);
141
+ }
130
142
  bindCore(actions, contextActions) {
131
143
  // Copy actions into the shared runtime (all extension APIs reference this)
132
144
  this.runtime.sendMessage = actions.sendMessage;
@@ -556,12 +568,8 @@ export class ExtensionRunner {
556
568
  systemPrompt: currentSystemPrompt,
557
569
  };
558
570
  const handlerResult = await this.withTimeout(handler(event, ctx), this.beforeAgentStartTimeoutMs);
559
- if (handlerResult === undefined) {
560
- this.emitError({
561
- extensionPath: ext.path,
562
- event: "before_agent_start",
563
- error: `handler timed out after ${this.beforeAgentStartTimeoutMs}ms`,
564
- });
571
+ if (handlerResult === this.beforeAgentStartTimeoutSentinel) {
572
+ this.reportBeforeAgentStartTimeout(ext.path);
565
573
  continue;
566
574
  }
567
575
  if (handlerResult) {
@@ -8,6 +8,9 @@ import type { ToolDefinition } from "./extensions/index.js";
8
8
  export declare class MCPManager {
9
9
  private client;
10
10
  private tools;
11
+ private enabledServerIds;
12
+ private startedServerIds;
13
+ private failedServerIds;
11
14
  constructor();
12
15
  /**
13
16
  * Initialize MCP manager and load tools
@@ -21,6 +24,12 @@ export declare class MCPManager {
21
24
  * Get the MCP client instance
22
25
  */
23
26
  getClient(): MCPClient;
27
+ getStatus(): {
28
+ enabledServers: string[];
29
+ startedServers: string[];
30
+ failedServers: string[];
31
+ toolCount: number;
32
+ };
24
33
  /**
25
34
  * Cleanup: stop all servers
26
35
  */
@@ -9,6 +9,9 @@ import { listEnabledMCPServers } from "./mcp/mcp-config.js";
9
9
  export class MCPManager {
10
10
  client;
11
11
  tools = [];
12
+ enabledServerIds = [];
13
+ startedServerIds = [];
14
+ failedServerIds = [];
12
15
  constructor() {
13
16
  this.client = new MCPClient();
14
17
  }
@@ -18,11 +21,23 @@ export class MCPManager {
18
21
  async initialize() {
19
22
  // Load enabled servers
20
23
  const enabledServers = listEnabledMCPServers();
24
+ this.enabledServerIds = enabledServers.map((s) => s.id);
25
+ this.startedServerIds = [];
26
+ this.failedServerIds = [];
21
27
  for (const serverConfig of enabledServers) {
22
28
  this.client.addServer(serverConfig);
23
29
  // Start stdio-based servers
24
30
  if (serverConfig.transport !== "sse") {
25
- await this.client.startServer(serverConfig.id);
31
+ const ok = await this.client.startServer(serverConfig.id);
32
+ if (ok) {
33
+ this.startedServerIds.push(serverConfig.id);
34
+ }
35
+ else {
36
+ this.failedServerIds.push(serverConfig.id);
37
+ }
38
+ }
39
+ else {
40
+ this.startedServerIds.push(serverConfig.id);
26
41
  }
27
42
  }
28
43
  // Load tools from all servers
@@ -40,6 +55,14 @@ export class MCPManager {
40
55
  getClient() {
41
56
  return this.client;
42
57
  }
58
+ getStatus() {
59
+ return {
60
+ enabledServers: [...this.enabledServerIds],
61
+ startedServers: [...this.startedServerIds],
62
+ failedServers: [...this.failedServerIds],
63
+ toolCount: this.tools.length,
64
+ };
65
+ }
43
66
  /**
44
67
  * Cleanup: stop all servers
45
68
  */
package/dist/core/sdk.js CHANGED
@@ -241,6 +241,16 @@ export async function createAgentSession(options = {}) {
241
241
  await mcpManager.initialize();
242
242
  mcpTools.push(...mcpManager.getTools());
243
243
  time("mcp.initialize");
244
+ const mcpStatus = mcpManager.getStatus();
245
+ if (mcpStatus.toolCount === 0) {
246
+ const failed = mcpStatus.failedServers.length > 0
247
+ ? ` failed=${mcpStatus.failedServers.join(",")}`
248
+ : "";
249
+ console.warn(`MCP enabled but no tools loaded (enabled=${mcpStatus.enabledServers.length}, started=${mcpStatus.startedServers.length}, tools=0).${failed}`);
250
+ }
251
+ else {
252
+ console.error(`MCP tools loaded: ${mcpStatus.toolCount}`);
253
+ }
244
254
  process.once("exit", () => mcpManager?.dispose());
245
255
  }
246
256
  catch (error) {
@@ -11,8 +11,24 @@ import { dirname } from "node:path";
11
11
  import { createRequire } from "node:module";
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
  const require = createRequire(import.meta.url);
14
- // Try to load from bundled packages first (dist/packages/nanosoul)
15
- const BUNDLED_SOUL = join(__dirname, "packages", "nanosoul");
14
+ function getBundledSoulCandidates() {
15
+ return [
16
+ // Published package runtime: dist/core -> dist/packages/nanosoul
17
+ join(__dirname, "..", "packages", "nanosoul"),
18
+ // Legacy/runtime fallback
19
+ join(__dirname, "packages", "nanosoul"),
20
+ // Dev workspace runtime
21
+ join(process.cwd(), "packages", "nanosoul", "dist"),
22
+ ];
23
+ }
24
+ function resolveBundledSoulEntry() {
25
+ for (const dir of getBundledSoulCandidates()) {
26
+ const entry = join(dir, "index.js");
27
+ if (existsSync(entry))
28
+ return entry;
29
+ }
30
+ return undefined;
31
+ }
16
32
  /**
17
33
  * Default Soul configuration for NanoPencil
18
34
  */
@@ -49,9 +65,10 @@ export function getSoulConfig() {
49
65
  */
50
66
  export async function createSoulManager() {
51
67
  try {
52
- // Try bundled package first (dist/packages/nanosoul)
53
- if (existsSync(BUNDLED_SOUL)) {
54
- const { SoulManager: SM } = await import(join(BUNDLED_SOUL, "index.js"));
68
+ // Try bundled package first
69
+ const bundledEntry = resolveBundledSoulEntry();
70
+ if (bundledEntry) {
71
+ const { SoulManager: SM } = await import(bundledEntry);
55
72
  return new SM({
56
73
  config: getSoulConfig(),
57
74
  });
@@ -72,9 +89,8 @@ export async function createSoulManager() {
72
89
  */
73
90
  export function isSoulAvailable() {
74
91
  // Check bundled version first
75
- if (existsSync(BUNDLED_SOUL)) {
76
- return existsSync(join(BUNDLED_SOUL, "index.js"));
77
- }
92
+ if (resolveBundledSoulEntry())
93
+ return true;
78
94
  // Fall back to checking node_modules
79
95
  try {
80
96
  require.resolve("nanosoul");
@@ -4020,6 +4020,34 @@ export class InteractiveMode {
4020
4020
  this.ui.requestRender();
4021
4021
  return;
4022
4022
  }
4023
+ if (action === "status" || action === "tools") {
4024
+ const runtimeTools = this.session
4025
+ .getAllTools()
4026
+ .filter((t) => t.name.startsWith("mcp_"));
4027
+ this.chatContainer.addChild(new Spacer(1));
4028
+ if (runtimeTools.length === 0) {
4029
+ this.chatContainer.addChild(new Text([
4030
+ theme.bold("MCP Runtime Status"),
4031
+ "",
4032
+ "No MCP tools are currently registered in this session.",
4033
+ theme.fg("dim", "Tip: run /reload and check startup logs for MCP errors."),
4034
+ ].join("\n"), 1, 0));
4035
+ }
4036
+ else {
4037
+ const lines = [
4038
+ theme.bold("MCP Runtime Status"),
4039
+ "",
4040
+ `Registered MCP tools: ${runtimeTools.length}`,
4041
+ ...runtimeTools.slice(0, 30).map((t) => `- ${t.name}`),
4042
+ ];
4043
+ if (runtimeTools.length > 30) {
4044
+ lines.push(theme.fg("dim", `...and ${runtimeTools.length - 30} more`));
4045
+ }
4046
+ this.chatContainer.addChild(new Text(lines.join("\n"), 1, 0));
4047
+ }
4048
+ this.ui.requestRender();
4049
+ return;
4050
+ }
4023
4051
  if ((action === "enable" || action === "disable") && target) {
4024
4052
  setMCPServerEnabled(target, action === "enable");
4025
4053
  this.chatContainer.addChild(new Spacer(1));
@@ -4028,7 +4056,7 @@ export class InteractiveMode {
4028
4056
  return;
4029
4057
  }
4030
4058
  this.chatContainer.addChild(new Spacer(1));
4031
- this.chatContainer.addChild(new Text("Usage: /mcp [list|enable <id>|disable <id>]", 1, 0));
4059
+ this.chatContainer.addChild(new Text("Usage: /mcp [list|status|tools|enable <id>|disable <id>]", 1, 0));
4032
4060
  this.ui.requestRender();
4033
4061
  }
4034
4062
  async handleUpdateCommand() {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * [INPUT]: InsightsReport, locale
3
- * [OUTPUT]: Complete standalone HTML string
4
- * [POS]: Pure render function — no side effects, no dependencies
3
+ * [OUTPUT]: Standalone HTML report
4
+ * [POS]: Pure renderer, no side effects
5
5
  */
6
6
  import type { InsightsReport } from "./types.js";
7
7
  export declare function renderInsightsHtml(report: InsightsReport, locale: string): string;
@@ -1,9 +1,77 @@
1
1
  /**
2
2
  * [INPUT]: InsightsReport, locale
3
- * [OUTPUT]: Complete standalone HTML string
4
- * [POS]: Pure render function — no side effects, no dependencies
3
+ * [OUTPUT]: Standalone HTML report
4
+ * [POS]: Pure renderer, no side effects
5
5
  */
6
6
  import { PROMPTS } from "./i18n.js";
7
+ const UI = {
8
+ en: {
9
+ subtitle: "sessions analyzed",
10
+ atAGlance: "At a Glance",
11
+ whatsWorking: "What's Working",
12
+ whatsHindering: "What's Hindering",
13
+ quickWins: "Quick Wins",
14
+ focusArea: "Focus Area",
15
+ noneYet: "No strong signal yet.",
16
+ workOn: "What You Work On",
17
+ workOnIntro: "Top project areas inferred from your memory graph.",
18
+ sessionWord: "sessions",
19
+ distribution: "Focus Distribution",
20
+ topProjects: "Top Projects",
21
+ frequentTags: "Frequent Tags",
22
+ successes: "Impressive Things You Solved",
23
+ successesIntro: "Resolved struggles and high-value lessons worth repeating.",
24
+ frictions: "Where Things Still Go Wrong",
25
+ frictionsIntro: "Open struggles that are still costing time.",
26
+ details: "Details",
27
+ weight: "Weight",
28
+ access: "Access",
29
+ importance: "Importance",
30
+ attempts: "Attempts",
31
+ solution: "Solution",
32
+ status: "Status",
33
+ resolved: "resolved",
34
+ unresolved: "open",
35
+ knowledgePrefs: "Knowledge and Preferences",
36
+ lessons: "Top Lessons",
37
+ knowledge: "Knowledge Base",
38
+ preferences: "User Preferences",
39
+ generatedBy: "Generated by NanoMem",
40
+ },
41
+ zh: {
42
+ subtitle: "已分析会话",
43
+ atAGlance: "总览",
44
+ whatsWorking: "做得好的地方",
45
+ whatsHindering: "当前阻碍",
46
+ quickWins: "快速改进",
47
+ focusArea: "主要方向",
48
+ noneYet: "暂时没有明显信号。",
49
+ workOn: "你在做什么",
50
+ workOnIntro: "基于记忆图谱推断的项目重点领域。",
51
+ sessionWord: "次会话",
52
+ distribution: "关注分布",
53
+ topProjects: "主要项目",
54
+ frequentTags: "高频标签",
55
+ successes: "你解决得很好的事",
56
+ successesIntro: "已解决问题与高价值经验,建议重复复用。",
57
+ frictions: "仍在反复消耗的点",
58
+ frictionsIntro: "尚未关闭的问题,持续影响效率。",
59
+ details: "详情",
60
+ weight: "权重",
61
+ access: "访问",
62
+ importance: "重要度",
63
+ attempts: "尝试",
64
+ solution: "解法",
65
+ status: "状态",
66
+ resolved: "已解决",
67
+ unresolved: "待处理",
68
+ knowledgePrefs: "知识与偏好",
69
+ lessons: "关键经验",
70
+ knowledge: "知识库",
71
+ preferences: "用户偏好",
72
+ generatedBy: "由 NanoMem 自动生成",
73
+ },
74
+ };
7
75
  function escapeHtml(str) {
8
76
  return str
9
77
  .replace(/&/g, "&amp;")
@@ -12,432 +80,352 @@ function escapeHtml(str) {
12
80
  .replace(/"/g, "&quot;")
13
81
  .replace(/'/g, "&#039;");
14
82
  }
15
- function renderStatsCards(report, locale) {
16
- const labels = locale === "zh"
17
- ? ["会话", "知识", "经验", "偏好", "事件", "面相"]
18
- : ["Sessions", "Knowledge", "Lessons", "Prefs", "Episodes", "Facets"];
19
- const values = [
20
- report.stats.totalSessions,
21
- report.stats.knowledge,
22
- report.stats.lessons,
23
- report.stats.preferences,
24
- report.stats.episodes,
25
- report.stats.facets,
26
- ];
27
- return values
28
- .map((v, i) => `
29
- <div class="stat-card">
30
- <div class="stat-value">${v}</div>
31
- <div class="stat-label">${labels[i]}</div>
32
- </div>`)
83
+ function formatDate(iso, locale) {
84
+ const d = new Date(iso);
85
+ if (Number.isNaN(d.getTime()))
86
+ return iso;
87
+ return d.toLocaleString(locale === "zh" ? "zh-CN" : "en-US");
88
+ }
89
+ function dedupeById(entries) {
90
+ const map = new Map();
91
+ for (const entry of entries)
92
+ map.set(entry.id, entry);
93
+ return [...map.values()];
94
+ }
95
+ function topTags(entries, limit = 8) {
96
+ const counts = new Map();
97
+ for (const entry of entries) {
98
+ for (const tag of entry.tags ?? [])
99
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
100
+ }
101
+ return [...counts.entries()]
102
+ .map(([label, value]) => ({ label, value }))
103
+ .sort((a, b) => b.value - a.value)
104
+ .slice(0, limit);
105
+ }
106
+ function topProjects(entries, limit = 6) {
107
+ const counts = new Map();
108
+ for (const entry of entries) {
109
+ if (!entry.project)
110
+ continue;
111
+ counts.set(entry.project, (counts.get(entry.project) ?? 0) + 1);
112
+ }
113
+ return [...counts.entries()]
114
+ .map(([label, value]) => ({ label, value }))
115
+ .sort((a, b) => b.value - a.value)
116
+ .slice(0, limit);
117
+ }
118
+ function renderBarRows(rows, colorClass, empty) {
119
+ if (!rows.length)
120
+ return `<p class="empty">${escapeHtml(empty)}</p>`;
121
+ const max = Math.max(...rows.map((row) => row.value), 1);
122
+ return rows
123
+ .map((row) => {
124
+ const width = Math.max(8, Math.round((row.value / max) * 100));
125
+ return `<div class="bar-row">
126
+ <div class="bar-label" title="${escapeHtml(row.label)}">${escapeHtml(row.label)}</div>
127
+ <div class="bar-track"><div class="bar-fill ${colorClass}" style="width:${width}%"></div></div>
128
+ <div class="bar-value">${row.value}</div>
129
+ </div>`;
130
+ })
33
131
  .join("");
34
132
  }
35
- function renderRecommendations(recommendations, p) {
36
- if (!recommendations.length)
37
- return "";
38
- return `
39
- <section class="section recommendations">
40
- <h2 class="section-title" onclick="toggleSection(this)">
41
- <span class="toggle-icon">&#9660;</span>
42
- ${escapeHtml(p.insightsSectionRecommendations)} (${recommendations.length})
43
- </h2>
44
- <div class="section-content">
45
- <ul class="recommendation-list">
46
- ${recommendations.map((r) => `<li>${escapeHtml(r)}</li>`).join("")}
47
- </ul>
48
- </div>
49
- </section>`;
133
+ function renderProjectAreas(projects, entries, ui) {
134
+ if (!projects.length)
135
+ return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
136
+ return projects
137
+ .map((project) => {
138
+ const scopeEntries = entries.filter((entry) => entry.project === project.label);
139
+ const tags = topTags(scopeEntries, 4).map((item) => item.label);
140
+ const desc = tags.length > 0
141
+ ? `Top signals: ${tags.map((tag) => `#${escapeHtml(tag)}`).join(" ")}`
142
+ : "No dominant tag signal yet.";
143
+ return `<article class="project-area">
144
+ <div class="area-header">
145
+ <span class="area-name">${escapeHtml(project.label)}</span>
146
+ <span class="area-count">~${project.value} ${escapeHtml(ui.sessionWord)}</span>
147
+ </div>
148
+ <p class="area-desc">${desc}</p>
149
+ </article>`;
150
+ })
151
+ .join("");
50
152
  }
51
- function renderPatterns(patterns, p) {
153
+ function renderPatternRows(patterns, ui) {
52
154
  if (!patterns.length)
53
- return "";
54
- const maxWeight = Math.max(...patterns.map((pa) => pa.weight), 1);
55
- return `
56
- <section class="section patterns">
57
- <h2 class="section-title" onclick="toggleSection(this)">
58
- <span class="toggle-icon">&#9660;</span>
59
- ${escapeHtml(p.insightsSectionPatterns)} (${patterns.length})
60
- </h2>
61
- <div class="section-content">
62
- ${patterns
63
- .map((pa) => `
64
- <div class="pattern-item">
65
- <div class="pattern-header">
66
- <span class="pattern-trigger">When ${escapeHtml(pa.trigger)}</span>
67
- <span class="pattern-arrow">&#8594;</span>
68
- <span class="pattern-behavior">${escapeHtml(pa.behavior)}</span>
69
- </div>
70
- <div class="weight-bar-container">
71
- <div class="weight-bar" style="width: ${(pa.weight / maxWeight) * 100}%"></div>
72
- <span class="weight-value">${pa.weight.toFixed(1)}</span>
73
- </div>
74
- <div class="access-badge">&#128065; ${pa.entry.accessCount}</div>
75
- </div>`)
76
- .join("")}
77
- </div>
78
- </section>`;
155
+ return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
156
+ const maxWeight = Math.max(...patterns.map((item) => item.weight), 1);
157
+ return patterns
158
+ .slice(0, 12)
159
+ .map((item) => {
160
+ const width = Math.max(8, Math.round((item.weight / maxWeight) * 100));
161
+ return `<article class="item-card">
162
+ <div class="item-title"><strong>${escapeHtml(item.trigger)}</strong> <span class="arrow">-></span> ${escapeHtml(item.behavior)}</div>
163
+ <div class="weight-track"><div class="weight-fill" style="width:${width}%"></div></div>
164
+ <div class="meta-row">
165
+ <span>${escapeHtml(ui.weight)}: ${item.weight.toFixed(2)}</span>
166
+ <span>${escapeHtml(ui.access)}: ${item.entry.accessCount}</span>
167
+ <span>${escapeHtml(ui.importance)}: ${item.entry.importance}</span>
168
+ </div>
169
+ </article>`;
170
+ })
171
+ .join("");
79
172
  }
80
- function renderStruggles(struggles, p) {
173
+ function renderStruggleRows(struggles, ui) {
81
174
  if (!struggles.length)
82
- return "";
83
- const unresolved = struggles.filter((s) => !s.resolved);
84
- const resolved = struggles.filter((s) => s.resolved);
85
- const renderStruggleItem = (s) => `
86
- <div class="struggle-item ${s.resolved ? "resolved" : "unresolved"}">
87
- <div class="struggle-problem">
88
- <span class="status-icon">${s.resolved ? "&#10003;" : "&#9888;"}</span>
89
- ${escapeHtml(s.problem)}
90
- </div>
91
- ${s.attempts.length
92
- ? `
93
- <div class="struggle-attempts">
94
- <span class="attempts-label">Tried:</span>
95
- <ul>${s.attempts.map((a) => `<li>${escapeHtml(a)}</li>`).join("")}</ul>
96
- </div>`
97
- : ""}
98
- ${s.solution ? `<div class="struggle-solution"><span class="solution-label">Solution:</span> ${escapeHtml(s.solution)}</div>` : ""}
99
- </div>`;
100
- return `
101
- <section class="section struggles">
102
- <h2 class="section-title" onclick="toggleSection(this)">
103
- <span class="toggle-icon">&#9660;</span>
104
- ${escapeHtml(p.insightsSectionStruggles)} (${struggles.length})
105
- </h2>
106
- <div class="section-content">
107
- <div class="tab-container">
108
- <button class="tab-btn active" onclick="switchTab(this, 'unresolved')">${escapeHtml(p.insightsUnresolved)} (${unresolved.length})</button>
109
- <button class="tab-btn" onclick="switchTab(this, 'resolved')">${escapeHtml(p.insightsResolved)} (${resolved.length})</button>
110
- </div>
111
- <div class="tab-content unresolved active">
112
- ${unresolved.length ? unresolved.map(renderStruggleItem).join("") : `<p class="empty-state">No unresolved struggles</p>`}
113
- </div>
114
- <div class="tab-content resolved">
115
- ${resolved.length ? resolved.map(renderStruggleItem).join("") : `<p class="empty-state">No resolved struggles</p>`}
116
- </div>
117
- </div>
118
- </section>`;
175
+ return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
176
+ return struggles
177
+ .slice(0, 12)
178
+ .map((item) => {
179
+ const attempts = item.attempts.length
180
+ ? `<div class="sub-line"><strong>${escapeHtml(ui.attempts)}:</strong> ${item.attempts.map((a) => escapeHtml(a)).join(" | ")}</div>`
181
+ : "";
182
+ const solution = item.solution
183
+ ? `<div class="sub-line"><strong>${escapeHtml(ui.solution)}:</strong> ${escapeHtml(item.solution)}</div>`
184
+ : "";
185
+ return `<article class="item-card ${item.resolved ? "ok" : "warn"}">
186
+ <div class="item-title">${escapeHtml(item.problem)}</div>
187
+ ${attempts}
188
+ ${solution}
189
+ <div class="meta-row">
190
+ <span>${escapeHtml(ui.status)}: ${item.resolved ? escapeHtml(ui.resolved) : escapeHtml(ui.unresolved)}</span>
191
+ <span>${escapeHtml(ui.weight)}: ${item.weight.toFixed(2)}</span>
192
+ </div>
193
+ </article>`;
194
+ })
195
+ .join("");
119
196
  }
120
- function renderLessons(lessons, p) {
121
- if (!lessons.length)
122
- return "";
123
- return `
124
- <section class="section lessons">
125
- <h2 class="section-title" onclick="toggleSection(this)">
126
- <span class="toggle-icon">&#9660;</span>
127
- ${escapeHtml(p.insightsSectionLessons)} (${lessons.length})
128
- </h2>
129
- <div class="section-content">
130
- <ul class="lessons-list">
131
- ${lessons
132
- .map((l) => `
133
- <li>
134
- <span class="lesson-content">${escapeHtml(l.content)}</span>
135
- <span class="importance-stars">${"&#9733;".repeat(Math.min(Math.ceil(l.importance / 2), 5))}</span>
136
- </li>`)
137
- .join("")}
138
- </ul>
139
- </div>
140
- </section>`;
197
+ function renderMemoryList(entries, empty) {
198
+ if (!entries.length)
199
+ return `<p class="empty">${escapeHtml(empty)}</p>`;
200
+ return `<ul class="list">${entries
201
+ .map((entry) => `<li>${escapeHtml(entry.content)}${entry.tags.length ? ` <span class="tags">${entry.tags.slice(0, 4).map((tag) => `#${escapeHtml(tag)}`).join(" ")}</span>` : ""}</li>`)
202
+ .join("")}</ul>`;
141
203
  }
142
- function renderKnowledge(knowledge, locale) {
143
- if (!knowledge.length)
144
- return "";
145
- const title = locale === "zh" ? "知识库" : "Knowledge Base";
146
- return `
147
- <section class="section knowledge">
148
- <h2 class="section-title" onclick="toggleSection(this)">
149
- <span class="toggle-icon">&#9660;</span>
150
- ${title} (${knowledge.length})
151
- </h2>
152
- <div class="section-content">
153
- <ul class="knowledge-list">
154
- ${knowledge
155
- .map((k) => `
156
- <li>
157
- <span class="knowledge-content">${escapeHtml(k.content)}</span>
158
- <div class="tags">${k.tags
159
- .slice(0, 5)
160
- .map((t) => `<span class="tag">${escapeHtml(t)}</span>`)
161
- .join("")}</div>
162
- </li>`)
163
- .join("")}
164
- </ul>
165
- </div>
166
- </section>`;
204
+ function renderAtAGlance(report, projects, ui) {
205
+ const unresolved = report.struggles.filter((item) => !item.resolved);
206
+ const resolved = report.struggles.filter((item) => item.resolved);
207
+ const topPattern = report.patterns[0];
208
+ const topRec = report.recommendations[0];
209
+ const topProject = projects[0];
210
+ const working = resolved.length > 0
211
+ ? `${resolved.length} struggles are already resolved; keep reusing those fixes.`
212
+ : report.topLessons.length
213
+ ? `${report.topLessons.length} lessons captured; convert the top ones into repeatable checklists.`
214
+ : ui.noneYet;
215
+ const hindering = unresolved.length
216
+ ? `${unresolved.length} open struggles remain. Most frequent: "${unresolved[0]?.problem ?? ""}".`
217
+ : "No unresolved struggles currently visible.";
218
+ const quickWins = topRec ? topRec : ui.noneYet;
219
+ const focus = topProject
220
+ ? `${topProject.label} appears most often (${topProject.value} memories).`
221
+ : topPattern
222
+ ? `Dominant behavior: when "${topPattern.trigger}", you often "${topPattern.behavior}".`
223
+ : ui.noneYet;
224
+ return `<section class="glance">
225
+ <h2>${escapeHtml(ui.atAGlance)}</h2>
226
+ <div class="glance-grid">
227
+ <article class="glance-card">
228
+ <h3>${escapeHtml(ui.whatsWorking)}</h3>
229
+ <p>${escapeHtml(working)}</p>
230
+ </article>
231
+ <article class="glance-card warn">
232
+ <h3>${escapeHtml(ui.whatsHindering)}</h3>
233
+ <p>${escapeHtml(hindering)}</p>
234
+ </article>
235
+ <article class="glance-card">
236
+ <h3>${escapeHtml(ui.quickWins)}</h3>
237
+ <p>${escapeHtml(quickWins)}</p>
238
+ </article>
239
+ <article class="glance-card">
240
+ <h3>${escapeHtml(ui.focusArea)}</h3>
241
+ <p>${escapeHtml(focus)}</p>
242
+ </article>
243
+ </div>
244
+ </section>`;
167
245
  }
168
- function renderPreferences(preferences, locale) {
169
- if (!preferences.length)
170
- return "";
171
- const title = locale === "zh" ? "用户偏好" : "User Preferences";
172
- return `
173
- <section class="section preferences">
174
- <h2 class="section-title" onclick="toggleSection(this)">
175
- <span class="toggle-icon">&#9660;</span>
176
- ${title} (${preferences.length})
177
- </h2>
178
- <div class="section-content">
179
- <ul class="preferences-list">
180
- ${preferences.map((pr) => `<li>${escapeHtml(pr.content)}</li>`).join("")}
181
- </ul>
182
- </div>
183
- </section>`;
246
+ function renderRecommendations(report, title, empty) {
247
+ if (!report.recommendations.length)
248
+ return `<section class="section"><h2>${escapeHtml(title)}</h2><p class="empty">${escapeHtml(empty)}</p></section>`;
249
+ return `<section id="section-recommendations" class="section">
250
+ <h2>${escapeHtml(title)}</h2>
251
+ <ul class="recommend-list">${report.recommendations.map((rec) => `<li>${escapeHtml(rec)}</li>`).join("")}</ul>
252
+ </section>`;
184
253
  }
185
254
  export function renderInsightsHtml(report, locale) {
186
- const p = PROMPTS[locale] ?? PROMPTS.en;
187
- const hasData = report.patterns.length || report.struggles.length || report.topLessons.length;
188
- const css = `
189
- :root {
190
- --bg: #ffffff;
191
- --bg-card: #f8f9fa;
192
- --text: #1a1a2e;
193
- --text-secondary: #6c757d;
194
- --border: #dee2e6;
195
- --accent-pattern: #3b82f6;
196
- --accent-struggle-unresolved: #f59e0b;
197
- --accent-struggle-resolved: #10b981;
198
- --accent-lesson: #8b5cf6;
199
- --accent-recommendation: #eab308;
200
- --shadow: rgba(0, 0, 0, 0.1);
201
- }
202
- [data-theme="dark"] {
203
- --bg: #1a1a2e;
204
- --bg-card: #16213e;
205
- --text: #e8e8e8;
206
- --text-secondary: #a0a0a0;
207
- --border: #374151;
208
- --shadow: rgba(0, 0, 0, 0.3);
209
- }
210
- * { box-sizing: border-box; margin: 0; padding: 0; }
211
- body {
212
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
213
- background: var(--bg);
214
- color: var(--text);
215
- line-height: 1.6;
216
- padding: 2rem;
217
- max-width: 1200px;
218
- margin: 0 auto;
219
- }
220
- header {
221
- display: flex;
222
- justify-content: space-between;
223
- align-items: center;
224
- margin-bottom: 2rem;
225
- padding-bottom: 1rem;
226
- border-bottom: 1px solid var(--border);
227
- }
228
- h1 { font-size: 1.75rem; font-weight: 600; }
229
- .meta { color: var(--text-secondary); font-size: 0.875rem; }
230
- .theme-toggle {
231
- background: var(--bg-card);
232
- border: 1px solid var(--border);
233
- padding: 0.5rem 1rem;
234
- border-radius: 0.5rem;
235
- cursor: pointer;
236
- font-size: 1.25rem;
237
- }
238
- .stats-row {
239
- display: grid;
240
- grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
241
- gap: 1rem;
242
- margin-bottom: 2rem;
243
- }
244
- .stat-card {
245
- background: var(--bg-card);
246
- padding: 1rem;
247
- border-radius: 0.75rem;
248
- text-align: center;
249
- border: 1px solid var(--border);
250
- }
251
- .stat-value { font-size: 2rem; font-weight: 700; color: var(--accent-pattern); }
252
- .stat-label { font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; }
253
- .section {
254
- background: var(--bg-card);
255
- border-radius: 0.75rem;
256
- margin-bottom: 1rem;
257
- border: 1px solid var(--border);
258
- overflow: hidden;
259
- }
260
- .section-title {
261
- padding: 1rem 1.25rem;
262
- cursor: pointer;
263
- display: flex;
264
- align-items: center;
265
- gap: 0.5rem;
266
- font-size: 1rem;
267
- font-weight: 600;
268
- user-select: none;
269
- }
270
- .section-title:hover { background: var(--bg); }
271
- .toggle-icon { font-size: 0.75rem; transition: transform 0.2s; }
272
- .section.collapsed .toggle-icon { transform: rotate(-90deg); }
273
- .section.collapsed .section-content { display: none; }
274
- .section-content { padding: 0 1.25rem 1.25rem; }
275
- .recommendations { border-left: 4px solid var(--accent-recommendation); }
276
- .recommendation-list { list-style: none; }
277
- .recommendation-list li {
278
- padding: 0.75rem;
279
- margin-bottom: 0.5rem;
280
- background: var(--bg);
281
- border-radius: 0.5rem;
282
- border-left: 3px solid var(--accent-recommendation);
283
- }
284
- .patterns { border-left: 4px solid var(--accent-pattern); }
285
- .pattern-item {
286
- padding: 1rem;
287
- margin-bottom: 0.75rem;
288
- background: var(--bg);
289
- border-radius: 0.5rem;
290
- position: relative;
291
- }
292
- .pattern-header { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
293
- .pattern-trigger { color: var(--accent-pattern); font-weight: 500; }
294
- .pattern-arrow { color: var(--text-secondary); }
295
- .pattern-behavior { font-weight: 500; }
296
- .weight-bar-container {
297
- margin-top: 0.5rem;
298
- display: flex;
299
- align-items: center;
300
- gap: 0.5rem;
301
- }
302
- .weight-bar {
303
- height: 6px;
304
- background: var(--accent-pattern);
305
- border-radius: 3px;
306
- flex-shrink: 0;
307
- }
308
- .weight-value { font-size: 0.75rem; color: var(--text-secondary); }
309
- .access-badge {
310
- position: absolute;
311
- top: 0.5rem;
312
- right: 0.5rem;
313
- font-size: 0.75rem;
314
- color: var(--text-secondary);
315
- }
316
- .struggles { border-left: 4px solid var(--accent-struggle-unresolved); }
317
- .tab-container { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
318
- .tab-btn {
319
- padding: 0.5rem 1rem;
320
- border: 1px solid var(--border);
321
- background: var(--bg);
322
- border-radius: 0.5rem;
323
- cursor: pointer;
324
- font-size: 0.875rem;
325
- }
326
- .tab-btn.active {
327
- background: var(--accent-pattern);
328
- color: white;
329
- border-color: var(--accent-pattern);
330
- }
331
- .tab-content { display: none; }
332
- .tab-content.active { display: block; }
333
- .struggle-item {
334
- padding: 1rem;
335
- margin-bottom: 0.75rem;
336
- background: var(--bg);
337
- border-radius: 0.5rem;
338
- border-left: 3px solid var(--accent-struggle-unresolved);
339
- }
340
- .struggle-item.resolved { border-left-color: var(--accent-struggle-resolved); }
341
- .struggle-problem { font-weight: 500; display: flex; align-items: center; gap: 0.5rem; }
342
- .status-icon { font-size: 1rem; }
343
- .struggle-item.unresolved .status-icon { color: var(--accent-struggle-unresolved); }
344
- .struggle-item.resolved .status-icon { color: var(--accent-struggle-resolved); }
345
- .struggle-attempts { margin-top: 0.5rem; font-size: 0.875rem; color: var(--text-secondary); }
346
- .struggle-attempts ul { margin-left: 1.5rem; margin-top: 0.25rem; }
347
- .struggle-solution {
348
- margin-top: 0.5rem;
349
- padding: 0.5rem;
350
- background: rgba(16, 185, 129, 0.1);
351
- border-radius: 0.25rem;
352
- font-size: 0.875rem;
353
- }
354
- .solution-label { font-weight: 500; color: var(--accent-struggle-resolved); }
355
- .lessons { border-left: 4px solid var(--accent-lesson); }
356
- .lessons-list { list-style: none; }
357
- .lessons-list li {
358
- padding: 0.75rem;
359
- margin-bottom: 0.5rem;
360
- background: var(--bg);
361
- border-radius: 0.5rem;
362
- display: flex;
363
- justify-content: space-between;
364
- align-items: center;
365
- }
366
- .importance-stars { color: var(--accent-lesson); }
367
- .knowledge-list, .preferences-list { list-style: none; }
368
- .knowledge-list li, .preferences-list li {
369
- padding: 0.75rem;
370
- margin-bottom: 0.5rem;
371
- background: var(--bg);
372
- border-radius: 0.5rem;
373
- }
374
- .tags { display: flex; gap: 0.25rem; margin-top: 0.5rem; flex-wrap: wrap; }
375
- .tag {
376
- font-size: 0.7rem;
377
- padding: 0.125rem 0.375rem;
378
- background: var(--border);
379
- border-radius: 0.25rem;
380
- color: var(--text-secondary);
381
- }
382
- .empty-state {
383
- text-align: center;
384
- padding: 2rem;
385
- color: var(--text-secondary);
386
- }
387
- `;
388
- const js = `
389
- function toggleSection(el) {
390
- el.parentElement.classList.toggle('collapsed');
391
- }
392
- function switchTab(btn, tabName) {
393
- const container = btn.closest('.section-content');
394
- container.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
395
- container.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
396
- btn.classList.add('active');
397
- container.querySelector('.tab-content.' + tabName).classList.add('active');
398
- }
399
- function toggleTheme() {
400
- const current = document.documentElement.getAttribute('data-theme');
401
- const next = current === 'dark' ? 'light' : 'dark';
402
- document.documentElement.setAttribute('data-theme', next);
403
- localStorage.setItem('nanomem-theme', next);
404
- document.querySelector('.theme-toggle').textContent = next === 'dark' ? '☀️' : '🌙';
405
- }
406
- (function() {
407
- const saved = localStorage.getItem('nanomem-theme') || 'light';
408
- document.documentElement.setAttribute('data-theme', saved);
409
- document.querySelector('.theme-toggle').textContent = saved === 'dark' ? '☀️' : '🌙';
410
- })();
411
- `;
412
- const body = hasData
413
- ? `
414
- <div class="stats-row">${renderStatsCards(report, locale)}</div>
415
- ${renderRecommendations(report.recommendations, p)}
416
- ${renderPatterns(report.patterns, p)}
417
- ${renderStruggles(report.struggles, p)}
418
- ${renderLessons(report.topLessons, p)}
419
- ${renderKnowledge(report.topKnowledge, locale)}
420
- ${renderPreferences(report.preferences, locale)}`
421
- : `<div class="empty-state">${escapeHtml(p.insightsNoData)}</div>`;
422
- return `<!DOCTYPE html>
423
- <html lang="${locale}">
424
- <head>
425
- <meta charset="UTF-8">
426
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
427
- <title>${escapeHtml(p.insightsTitle)}</title>
428
- <style>${css}</style>
429
- </head>
430
- <body>
431
- <header>
432
- <div>
433
- <h1>${escapeHtml(p.insightsTitle)}</h1>
434
- <div class="meta">${escapeHtml(p.insightsGeneratedAt)}: ${report.generatedAt}</div>
435
- </div>
436
- <button class="theme-toggle" onclick="toggleTheme()">🌙</button>
437
- </header>
438
- ${body}
439
- <script>${js}</script>
440
- </body>
255
+ const isZh = locale === "zh";
256
+ const ui = UI[isZh ? "zh" : "en"];
257
+ const prompts = PROMPTS[locale] ?? PROMPTS.en;
258
+ const allEntries = dedupeById([...report.topKnowledge, ...report.topLessons, ...report.preferences]);
259
+ const projects = topProjects(allEntries);
260
+ const tags = topTags(allEntries);
261
+ const unresolved = report.struggles.filter((item) => !item.resolved);
262
+ const resolved = report.struggles.filter((item) => item.resolved);
263
+ const statsRow = [
264
+ { label: "Sessions", value: report.stats.totalSessions },
265
+ { label: "Knowledge", value: report.stats.knowledge },
266
+ { label: "Lessons", value: report.stats.lessons },
267
+ { label: "Preferences", value: report.stats.preferences },
268
+ { label: "Struggles", value: report.struggles.length },
269
+ { label: "Patterns", value: report.patterns.length },
270
+ ];
271
+ const css = `
272
+ *{box-sizing:border-box}
273
+ body{margin:0;padding:48px 24px;font-family:Inter,"Segoe UI",Arial,sans-serif;background:#f8fafc;color:#334155;line-height:1.65}
274
+ .container{max-width:960px;margin:0 auto}
275
+ h1{font-size:34px;font-weight:700;color:#0f172a;margin:0 0 8px}
276
+ h2{font-size:22px;font-weight:700;color:#0f172a;margin:0 0 14px}
277
+ h3{font-size:15px;font-weight:700;color:#1e293b;margin:0 0 8px}
278
+ .subtitle{color:#64748b;font-size:14px;margin:0 0 24px}
279
+ .toc{display:flex;flex-wrap:wrap;gap:8px;padding:14px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;margin:0 0 24px}
280
+ .toc a{font-size:12px;color:#475569;background:#f1f5f9;border-radius:6px;padding:6px 10px;text-decoration:none}
281
+ .toc a:hover{background:#e2e8f0}
282
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px;padding:20px 0;border-top:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;margin:0 0 28px}
283
+ .stat{text-align:center}
284
+ .stat-value{font-size:24px;font-weight:700;color:#0f172a}
285
+ .stat-label{font-size:11px;color:#64748b;text-transform:uppercase}
286
+ .glance{background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);border:1px solid #f59e0b;border-radius:12px;padding:20px 22px;margin:0 0 28px}
287
+ .glance-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
288
+ .glance-card{background:rgba(255,255,255,.62);border:1px solid rgba(245,158,11,.28);border-radius:8px;padding:12px}
289
+ .glance-card.warn{background:rgba(255,241,242,.82);border-color:#fca5a5}
290
+ .glance-card p{margin:0;font-size:13px;color:#78350f}
291
+ .section{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:18px;margin:0 0 16px}
292
+ .section-intro{font-size:13px;color:#64748b;margin:0 0 12px}
293
+ .project-list{display:flex;flex-direction:column;gap:10px}
294
+ .project-area{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px}
295
+ .area-header{display:flex;justify-content:space-between;align-items:center;gap:10px}
296
+ .area-name{font-size:15px;font-weight:700;color:#0f172a}
297
+ .area-count{font-size:12px;color:#64748b;background:#f1f5f9;padding:2px 8px;border-radius:4px}
298
+ .area-desc{margin:8px 0 0;font-size:13px;color:#475569}
299
+ .charts{display:grid;grid-template-columns:1fr 1fr;gap:16px}
300
+ .chart{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:14px}
301
+ .chart-title{font-size:12px;color:#64748b;text-transform:uppercase;margin:0 0 10px}
302
+ .bar-row{display:flex;align-items:center;gap:8px;margin-bottom:7px}
303
+ .bar-label{width:120px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:#475569}
304
+ .bar-track{flex:1;height:7px;background:#f1f5f9;border-radius:99px}
305
+ .bar-fill{height:100%;border-radius:99px}
306
+ .bar-fill.blue{background:#2563eb}
307
+ .bar-fill.cyan{background:#0891b2}
308
+ .bar-value{width:24px;text-align:right;font-size:12px;color:#64748b}
309
+ .recommend-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}
310
+ .recommend-list li{background:#f0fdf4;border:1px solid #bbf7d0;border-left:4px solid #16a34a;border-radius:8px;padding:11px 12px;font-size:14px}
311
+ .item-grid{display:flex;flex-direction:column;gap:10px}
312
+ .item-card{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:12px}
313
+ .item-card.ok{background:#f0fdf4;border-color:#86efac}
314
+ .item-card.warn{background:#fef2f2;border-color:#fca5a5}
315
+ .item-title{font-size:14px;color:#0f172a}
316
+ .arrow{color:#94a3b8}
317
+ .weight-track{margin-top:8px;height:6px;background:#e2e8f0;border-radius:99px}
318
+ .weight-fill{height:100%;background:#2563eb;border-radius:99px}
319
+ .sub-line{margin-top:8px;font-size:13px;color:#334155}
320
+ .meta-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px;color:#64748b}
321
+ details{border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;padding:10px 12px;margin-bottom:10px}
322
+ summary{cursor:pointer;font-size:14px;font-weight:700;color:#334155}
323
+ .list{margin:10px 0 0;padding-left:18px}
324
+ .list li{margin:0 0 8px;font-size:13px;color:#334155}
325
+ .tags{font-size:12px;color:#64748b}
326
+ .empty{font-size:13px;color:#94a3b8}
327
+ footer{margin-top:22px;text-align:center;font-size:12px;color:#94a3b8}
328
+ @media (max-width:760px){body{padding:28px 14px}.charts{grid-template-columns:1fr}.bar-label{width:96px}}
329
+ `;
330
+ return `<!doctype html>
331
+ <html lang="${isZh ? "zh-CN" : "en"}">
332
+ <head>
333
+ <meta charset="utf-8" />
334
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
335
+ <title>${escapeHtml(prompts.insightsTitle)}</title>
336
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
337
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
338
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
339
+ <style>${css}</style>
340
+ </head>
341
+ <body>
342
+ <main class="container">
343
+ <h1>${escapeHtml(prompts.insightsTitle)}</h1>
344
+ <p class="subtitle">${report.stats.totalSessions} ${escapeHtml(ui.subtitle)} | ${escapeHtml(prompts.insightsGeneratedAt)}: ${escapeHtml(formatDate(report.generatedAt, locale))}</p>
345
+
346
+ <nav class="toc">
347
+ <a href="#section-work">${escapeHtml(ui.workOn)}</a>
348
+ <a href="#section-distribution">${escapeHtml(ui.distribution)}</a>
349
+ <a href="#section-recommendations">${escapeHtml(prompts.insightsSectionRecommendations)}</a>
350
+ <a href="#section-patterns">${escapeHtml(prompts.insightsSectionPatterns)}</a>
351
+ <a href="#section-struggles">${escapeHtml(prompts.insightsSectionStruggles)}</a>
352
+ <a href="#section-memory">${escapeHtml(ui.knowledgePrefs)}</a>
353
+ </nav>
354
+
355
+ <section class="stats">
356
+ ${statsRow
357
+ .map((item) => `<div class="stat"><div class="stat-value">${item.value}</div><div class="stat-label">${escapeHtml(item.label)}</div></div>`)
358
+ .join("")}
359
+ </section>
360
+
361
+ ${renderAtAGlance(report, projects, ui)}
362
+
363
+ <section id="section-work" class="section">
364
+ <h2>${escapeHtml(ui.workOn)}</h2>
365
+ <p class="section-intro">${escapeHtml(ui.workOnIntro)}</p>
366
+ <div class="project-list">${renderProjectAreas(projects, allEntries, ui)}</div>
367
+ </section>
368
+
369
+ <section id="section-distribution" class="section">
370
+ <h2>${escapeHtml(ui.distribution)}</h2>
371
+ <div class="charts">
372
+ <div class="chart">
373
+ <p class="chart-title">${escapeHtml(ui.topProjects)}</p>
374
+ ${renderBarRows(projects, "blue", ui.noneYet)}
375
+ </div>
376
+ <div class="chart">
377
+ <p class="chart-title">${escapeHtml(ui.frequentTags)}</p>
378
+ ${renderBarRows(tags, "cyan", ui.noneYet)}
379
+ </div>
380
+ </div>
381
+ </section>
382
+
383
+ ${renderRecommendations(report, prompts.insightsSectionRecommendations, prompts.insightsNoData)}
384
+
385
+ <section class="section">
386
+ <h2>${escapeHtml(ui.successes)}</h2>
387
+ <p class="section-intro">${escapeHtml(ui.successesIntro)}</p>
388
+ <div class="item-grid">
389
+ ${renderStruggleRows(resolved, ui)}
390
+ ${renderMemoryList(report.topLessons.slice(0, 5), ui.noneYet)}
391
+ </div>
392
+ </section>
393
+
394
+ <section class="section">
395
+ <h2>${escapeHtml(ui.frictions)}</h2>
396
+ <p class="section-intro">${escapeHtml(ui.frictionsIntro)}</p>
397
+ <div class="item-grid">${renderStruggleRows(unresolved, ui)}</div>
398
+ </section>
399
+
400
+ <section id="section-patterns" class="section">
401
+ <h2>${escapeHtml(prompts.insightsSectionPatterns)}</h2>
402
+ <div class="item-grid">${renderPatternRows(report.patterns, ui)}</div>
403
+ </section>
404
+
405
+ <section id="section-struggles" class="section">
406
+ <h2>${escapeHtml(prompts.insightsSectionStruggles)}</h2>
407
+ <div class="item-grid">${renderStruggleRows(report.struggles, ui)}</div>
408
+ </section>
409
+
410
+ <section id="section-memory" class="section">
411
+ <h2>${escapeHtml(ui.knowledgePrefs)}</h2>
412
+ <details open>
413
+ <summary>${escapeHtml(ui.lessons)} (${report.topLessons.length})</summary>
414
+ ${renderMemoryList(report.topLessons.slice(0, 12), ui.noneYet)}
415
+ </details>
416
+ <details>
417
+ <summary>${escapeHtml(ui.knowledge)} (${report.topKnowledge.length})</summary>
418
+ ${renderMemoryList(report.topKnowledge.slice(0, 12), ui.noneYet)}
419
+ </details>
420
+ <details>
421
+ <summary>${escapeHtml(ui.preferences)} (${report.preferences.length})</summary>
422
+ ${renderMemoryList(report.preferences.slice(0, 12), ui.noneYet)}
423
+ </details>
424
+ </section>
425
+
426
+ <footer>${escapeHtml(ui.generatedBy)}</footer>
427
+ </main>
428
+ </body>
441
429
  </html>`;
442
430
  }
443
431
  //# sourceMappingURL=insights-html.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pencil-agent/nano-pencil",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "CLI writing agent with read, bash, edit, write tools and session management. Based on pi; supports DashScope Coding Plan. Soul enabled by default for AI personality evolution.",
5
5
  "type": "module",
6
6
  "bin": {