@kvidzibo/pi-afterword 0.6.0 → 0.7.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ - Show each afterword's generating model and thinking level beside its ID and date, and in details.
6
+ - Save generation thinking levels; automatically upgrade existing databases, leaving older levels unknown.
7
+
3
8
  ## 0.6.0
4
9
 
5
10
  - First standalone Pi package, extracted without behavior changes from the local extension.
package/README.md CHANGED
@@ -26,21 +26,24 @@ pi install npm:@kvidzibo/pi-afterword
26
26
 
27
27
  Run `/reload` in existing sessions. If migrating from the standalone
28
28
  `pi-afterword.ts`, remove its old `extensions` entry first: do not load both.
29
- Existing notes stay in the same database; no migration is needed.
29
+ Existing notes stay in the same database; schema upgrades are automatic.
30
30
 
31
31
  ## Use
32
32
 
33
33
  At interactive startup, `Afterwords: N` shows the saved-note count without adding
34
34
  model context. `/pi-afterword` opens a read-only list, newest first. Arrows select,
35
35
  Page Up/Down scroll, Enter shows details, and Escape returns or closes. Browsing
36
- makes no model requests and does not inject notes into the conversation.
36
+ makes no model requests and does not inject notes into the conversation. Each note's
37
+ header shows its generating model and thinking level; older notes without a saved
38
+ thinking level show `unknown`.
37
39
 
38
40
  Generation requires **10 assistant turns on the active branch**, including
39
41
  tool-call responses and compacted turns. Eligible interactive quit (including
40
42
  handled SIGTERM/SIGHUP) and successful Pinote Done request one lesson of at most
41
- 30 whitespace-separated words from the current model, with minimal reasoning and
42
- no tools. It prefers verified causes/fixes, surprising constraints or failed
43
- approaches; otherwise it records an important decision and its rationale.
43
+ 30 whitespace-separated words from the current model, with minimal reasoning
44
+ (adjusted to supported levels; off for non-reasoning models) and no tools. It
45
+ prefers verified causes/fixes, surprising constraints or failed approaches;
46
+ otherwise it records an important decision and its rationale.
44
47
 
45
48
  The request uses up to the last 64,000 conversation characters, less for small
46
49
  context windows; images, thinking and system instructions are omitted. There is
@@ -62,8 +65,8 @@ service or runtime dependency is required. The directory and database use
62
65
  owner-only permissions. Stop Pi sessions before backing up or moving the database.
63
66
 
64
67
  The database is **local and unencrypted**. Notes and metadata (session ID,
65
- conversation hash, project directory, model, timestamp, trigger and optional task
66
- ID) can contain sensitive project information despite the no-secrets instruction.
68
+ conversation hash, project directory, model, thinking level, timestamp, trigger
69
+ and optional task ID) can contain sensitive project information despite the no-secrets instruction.
67
70
  Generation sends conversation excerpts to your configured model provider.
68
71
 
69
72
  ## Development
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { chmodSync, closeSync, mkdirSync, openSync, statSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { clampThinkingLevel } from "@earendil-works/pi-ai";
4
5
  import { convertToLlm, DynamicBorder, ExtensionSelectorComponent, getAgentDir, keyHint, rawKeyHint, serializeConversation, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
6
  import { Container, ScrollView, Spacer, Text } from "@earendil-works/pi-tui";
6
7
 
@@ -31,10 +32,14 @@ async function database() {
31
32
  reason TEXT NOT NULL,
32
33
  cwd TEXT NOT NULL,
33
34
  model TEXT NOT NULL,
35
+ thinking_level TEXT,
34
36
  created_at TEXT NOT NULL,
35
37
  note TEXT NOT NULL,
36
38
  UNIQUE (session_id, context_hash)
37
39
  )`);
40
+ if (!db.prepare("PRAGMA table_info(afterwords)").all().some((column) => column.name === "thinking_level")) {
41
+ db.exec("ALTER TABLE afterwords ADD COLUMN thinking_level TEXT");
42
+ }
38
43
  return db;
39
44
  } catch (error) { db.close(); throw error; }
40
45
  }
@@ -70,12 +75,13 @@ async function record(ctx: ExtensionContext, state: NonNullable<ReturnType<typeo
70
75
  const maxChars = Math.min(CONTEXT_CHARS, Math.floor((model.contextWindow - 2048) / 2));
71
76
  if (maxChars < 256) throw new Error("Session context window too small");
72
77
  const history = conversation.length > maxChars ? `[Earlier conversation omitted]\n${conversation.slice(-maxChars)}` : conversation;
78
+ const thinkingLevel = clampThinkingLevel(model, "minimal");
73
79
  const result = await ctx.modelRegistry.streamSimple(model, {
74
80
  messages: [
75
81
  { role: "system", content: INSTRUCTION, timestamp: Date.now() },
76
82
  { role: "user", content: history, timestamp: Date.now() },
77
83
  ],
78
- }, { reasoning: "minimal", maxTokens: 1024, cacheRetention: "none", sessionId: randomUUID(), signal }).result();
84
+ }, { reasoning: thinkingLevel === "off" ? undefined : thinkingLevel, maxTokens: 1024, cacheRetention: "none", sessionId: randomUUID(), signal }).result();
79
85
  check();
80
86
  if (result.stopReason !== "stop") throw new Error("Incomplete afterword");
81
87
  const note = result.content.filter((block) => block.type === "text").map((block) => block.text).join(" ").replace(/\s+/gu, " ").trim();
@@ -84,8 +90,8 @@ async function record(ctx: ExtensionContext, state: NonNullable<ReturnType<typeo
84
90
  try {
85
91
  db.exec("BEGIN IMMEDIATE"); // Never wait for locks during shutdown.
86
92
  check();
87
- db.prepare("INSERT OR IGNORE INTO afterwords (session_id, context_hash, task_id, reason, cwd, model, created_at, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
88
- .run(sessionId, hash, taskId ?? null, reason, cwd, `${model.provider}/${model.id}`, new Date().toISOString(), note);
93
+ db.prepare("INSERT OR IGNORE INTO afterwords (session_id, context_hash, task_id, reason, cwd, model, thinking_level, created_at, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
94
+ .run(sessionId, hash, taskId ?? null, reason, cwd, `${result.provider ?? model.provider}/${result.responseModel ?? result.model ?? model.id}`, result.thinkingLevel ?? thinkingLevel, new Date().toISOString(), note);
89
95
  check();
90
96
  db.exec("COMMIT");
91
97
  } catch (error) {
@@ -107,7 +113,7 @@ export default function (pi: ExtensionAPI): void {
107
113
  try {
108
114
  const db = await database();
109
115
  try {
110
- notes = db.prepare("SELECT id, note, created_at, cwd, reason, task_id FROM afterwords ORDER BY created_at DESC, id DESC").all();
116
+ notes = db.prepare("SELECT id, note, created_at, cwd, reason, task_id, model, thinking_level FROM afterwords ORDER BY created_at DESC, id DESC").all();
111
117
  } finally { db.close(); }
112
118
  } catch {
113
119
  ctx.ui.notify("Saved afterwords could not be read.", "error");
@@ -153,7 +159,7 @@ export default function (pi: ExtensionAPI): void {
153
159
  for (const [i, row] of notes.entries()) {
154
160
  starts.push(lines.length);
155
161
  const active = i === selected;
156
- const label = theme.fg(active ? "accent" : "muted", `#${row.id} · ${String(row.created_at).slice(0, 16).replace("T", " ")}`);
162
+ const label = theme.fg(active ? "accent" : "muted", `#${row.id} · ${String(row.created_at).slice(0, 16).replace("T", " ")} · ${row.model} · thinking: ${row.thinking_level ?? "unknown"}`);
157
163
  const text = new Text(`${label}\n${theme.fg(active ? "accent" : "text", String(row.note))}`, 0, 0);
158
164
  for (const [j, line] of text.render(width - indent).entries()) {
159
165
  const prefix = active && j === 0 && indent === 3 ? theme.fg("accent", " → ") : " ".repeat(indent);
@@ -198,7 +204,7 @@ export default function (pi: ExtensionAPI): void {
198
204
  if (index === undefined) return;
199
205
  const row = notes[index];
200
206
  await ctx.ui.custom<void>((tui, _theme, keybindings, done) => {
201
- const text = new Text(`${row.note}\n\n${row.created_at} · ${row.reason}${row.task_id === null ? "" : ` · task ${row.task_id}`}\n${row.cwd}`, 0, 0);
207
+ const text = new Text(`${row.note}\n\n${row.created_at} · ${row.model} · thinking: ${row.thinking_level ?? "unknown"}\n${row.reason}${row.task_id === null ? "" : ` · task ${row.task_id}`}\n${row.cwd}`, 0, 0);
202
208
  const scroll = new ScrollView(text, { scrollbar: "hidden" });
203
209
  return {
204
210
  capturesViewportInput: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kvidzibo/pi-afterword",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Save durable conversation lessons on Pi quit or Pinote Done, and browse them with /pi-afterword.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,10 +41,14 @@
41
41
  "prepublishOnly": "npm test"
42
42
  },
43
43
  "peerDependencies": {
44
+ "@earendil-works/pi-ai": "*",
44
45
  "@earendil-works/pi-coding-agent": "*",
45
46
  "@earendil-works/pi-tui": "*"
46
47
  },
47
48
  "peerDependenciesMeta": {
49
+ "@earendil-works/pi-ai": {
50
+ "optional": true
51
+ },
48
52
  "@earendil-works/pi-coding-agent": {
49
53
  "optional": true
50
54
  },
@@ -53,6 +57,7 @@
53
57
  }
54
58
  },
55
59
  "devDependencies": {
60
+ "@earendil-works/pi-ai": "1.5.0",
56
61
  "@earendil-works/pi-coding-agent": "1.5.0",
57
62
  "@earendil-works/pi-tui": "1.5.0"
58
63
  }