pi-sessions 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/extensions/session-ask.ts +297 -0
  4. package/extensions/session-auto-title/command.ts +109 -0
  5. package/extensions/session-auto-title/context.ts +41 -0
  6. package/extensions/session-auto-title/controller.ts +298 -0
  7. package/extensions/session-auto-title/model.ts +61 -0
  8. package/extensions/session-auto-title/prompt.ts +54 -0
  9. package/extensions/session-auto-title/retitle.ts +641 -0
  10. package/extensions/session-auto-title/state.ts +69 -0
  11. package/extensions/session-auto-title/wizard.ts +583 -0
  12. package/extensions/session-auto-title.ts +209 -0
  13. package/extensions/session-handoff/extract.ts +278 -0
  14. package/extensions/session-handoff/metadata.ts +96 -0
  15. package/extensions/session-handoff/picker.ts +394 -0
  16. package/extensions/session-handoff/query.ts +318 -0
  17. package/extensions/session-handoff/refs.ts +151 -0
  18. package/extensions/session-handoff/review.ts +268 -0
  19. package/extensions/session-handoff.ts +203 -0
  20. package/extensions/session-hooks.ts +55 -0
  21. package/extensions/session-index.ts +159 -0
  22. package/extensions/session-search/extract.ts +997 -0
  23. package/extensions/session-search/hooks.ts +350 -0
  24. package/extensions/session-search/normalize.ts +170 -0
  25. package/extensions/session-search/reindex.ts +93 -0
  26. package/extensions/session-search.ts +390 -0
  27. package/extensions/shared/search-snippet.ts +40 -0
  28. package/extensions/shared/session-index/common.ts +222 -0
  29. package/extensions/shared/session-index/index.ts +5 -0
  30. package/extensions/shared/session-index/lineage.ts +417 -0
  31. package/extensions/shared/session-index/schema.ts +178 -0
  32. package/extensions/shared/session-index/search.ts +688 -0
  33. package/extensions/shared/session-index/store.ts +173 -0
  34. package/extensions/shared/session-ui.ts +15 -0
  35. package/extensions/shared/settings.ts +141 -0
  36. package/extensions/shared/time.ts +38 -0
  37. package/extensions/shared/typebox.ts +61 -0
  38. package/images/handoff.png +0 -0
  39. package/images/session-title.png +0 -0
  40. package/images/session_ask.png +0 -0
  41. package/images/session_picker.png +0 -0
  42. package/images/session_search.png +0 -0
  43. package/package.json +64 -0
@@ -0,0 +1,159 @@
1
+ import {
2
+ BorderedLoader,
3
+ type ExtensionAPI,
4
+ type ExtensionCommandContext,
5
+ type Theme,
6
+ } from "@mariozechner/pi-coding-agent";
7
+ import { type Focusable, matchesKey, visibleWidth } from "@mariozechner/pi-tui";
8
+ import { type ReindexResult, rebuildSessionIndex } from "./session-search/reindex.js";
9
+ import { getIndexStatus, type SessionIndexStatus } from "./shared/session-index/index.js";
10
+ import { loadSettings } from "./shared/settings.js";
11
+
12
+ type SessionIndexAction = "reindex" | undefined;
13
+
14
+ export default function sessionIndexExtension(pi: ExtensionAPI): void {
15
+ const settings = loadSettings();
16
+
17
+ pi.registerCommand("session-index", {
18
+ description: "Open the session index control panel",
19
+ handler: async (_args, ctx) => {
20
+ if (!ctx.hasUI) {
21
+ ctx.ui.notify("/session-index requires interactive mode.", "warning");
22
+ return;
23
+ }
24
+
25
+ const indexPath = settings.index.path;
26
+ const status = getIndexStatus(indexPath);
27
+ const action = await ctx.ui.custom<SessionIndexAction>(
28
+ (_tui, theme, _keybindings, done) => new SessionIndexPanel(theme, status, done),
29
+ {
30
+ overlay: true,
31
+ overlayOptions: {
32
+ anchor: "center",
33
+ width: 72,
34
+ margin: 1,
35
+ },
36
+ },
37
+ );
38
+
39
+ if (action !== "reindex") {
40
+ return;
41
+ }
42
+
43
+ const confirmed = await ctx.ui.confirm(
44
+ "Rebuild session index?",
45
+ "This rebuilds the entire session index from disk. Continue?",
46
+ );
47
+ if (!confirmed) {
48
+ ctx.ui.notify("Reindex cancelled.", "info");
49
+ return;
50
+ }
51
+
52
+ const result = await runReindexWithLoader(ctx, indexPath);
53
+ if (!result) {
54
+ ctx.ui.notify("Reindex cancelled.", "info");
55
+ return;
56
+ }
57
+
58
+ ctx.ui.notify(
59
+ `Indexed ${result.sessionCount} sessions and ${result.chunkCount} text chunks.`,
60
+ "info",
61
+ );
62
+ },
63
+ });
64
+ }
65
+
66
+ class SessionIndexPanel implements Focusable {
67
+ readonly width = 72;
68
+ focused = false;
69
+
70
+ invalidate(): void {}
71
+
72
+ constructor(
73
+ private readonly theme: Theme,
74
+ private readonly status: SessionIndexStatus,
75
+ private readonly done: (result: SessionIndexAction) => void,
76
+ ) {}
77
+
78
+ handleInput(data: string): void {
79
+ if (isCloseKey(data)) {
80
+ this.done(undefined);
81
+ return;
82
+ }
83
+
84
+ if (isReindexKey(data)) {
85
+ this.done("reindex");
86
+ }
87
+ }
88
+
89
+ render(_width: number): string[] {
90
+ const innerWidth = this.width - 2;
91
+ const lines: string[] = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
92
+
93
+ lines.push(
94
+ this.renderRow(innerWidth, ` ${this.theme.bold(this.theme.fg("accent", "Session Index"))}`),
95
+ );
96
+ lines.push(this.renderRow(innerWidth, ""));
97
+ lines.push(
98
+ this.renderRow(
99
+ innerWidth,
100
+ ` Path: ${this.status.exists ? this.status.dbPath : "<no index found>"}`,
101
+ ),
102
+ );
103
+ lines.push(
104
+ this.renderRow(
105
+ innerWidth,
106
+ ` Schema version: ${this.status.schemaVersion !== undefined ? String(this.status.schemaVersion) : "n/a"}`,
107
+ ),
108
+ );
109
+ lines.push(
110
+ this.renderRow(
111
+ innerWidth,
112
+ ` Session count: ${this.status.sessionCount !== undefined ? String(this.status.sessionCount) : "n/a"}`,
113
+ ),
114
+ );
115
+ lines.push(
116
+ this.renderRow(innerWidth, ` Last full reindex: ${this.status.lastFullReindexAt ?? "n/a"}`),
117
+ );
118
+ lines.push(this.renderRow(innerWidth, ""));
119
+ lines.push(this.renderRow(innerWidth, ` ${this.theme.fg("accent", "R")} rebuild from disk`));
120
+ lines.push(this.renderRow(innerWidth, ` ${this.theme.fg("dim", "Enter / Esc")} close`));
121
+ lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
122
+ return lines;
123
+ }
124
+
125
+ private renderRow(innerWidth: number, content: string): string {
126
+ const pad = Math.max(0, innerWidth - visibleWidth(content));
127
+ return `${this.theme.fg("border", "│")}${content}${" ".repeat(pad)}${this.theme.fg("border", "│")}`;
128
+ }
129
+ }
130
+
131
+ async function runReindexWithLoader(
132
+ ctx: ExtensionCommandContext,
133
+ indexPath: string,
134
+ ): Promise<ReindexResult | null> {
135
+ return ctx.ui.custom<ReindexResult | null>((tui, theme, _keybindings, done) => {
136
+ const loader = new BorderedLoader(tui, theme, "Rebuilding session index...");
137
+ loader.onAbort = () => done(null);
138
+
139
+ void (async () => {
140
+ try {
141
+ const result = await rebuildSessionIndex({ indexPath });
142
+ done(result);
143
+ } catch (error) {
144
+ ctx.ui.notify(`Reindex failed: ${String(error)}`, "error");
145
+ done(null);
146
+ }
147
+ })();
148
+
149
+ return loader;
150
+ });
151
+ }
152
+
153
+ function isCloseKey(data: string): boolean {
154
+ return matchesKey(data, "escape") || matchesKey(data, "enter");
155
+ }
156
+
157
+ function isReindexKey(data: string): boolean {
158
+ return data === "r" || data === "R" || matchesKey(data, "r");
159
+ }