@diegopetrucci/pi-extensions 0.1.74 → 0.1.78

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 (108) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +17 -12
  3. package/extensions/agent-workflow-audit/README.md +1 -1
  4. package/extensions/agent-workflow-audit/index.ts +1631 -1009
  5. package/extensions/agent-workflow-audit/package.json +1 -1
  6. package/extensions/annotate-git-diff/README.md +3 -1
  7. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  8. package/extensions/annotate-git-diff/git.ts +846 -768
  9. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  10. package/extensions/annotate-git-diff/index.ts +712 -671
  11. package/extensions/annotate-git-diff/package.json +1 -1
  12. package/extensions/annotate-git-diff/prompt.ts +45 -45
  13. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  14. package/extensions/annotate-git-diff/review-server.ts +337 -311
  15. package/extensions/annotate-git-diff/types.ts +132 -132
  16. package/extensions/annotate-git-diff/ui.ts +27 -27
  17. package/extensions/annotate-git-diff/watch.ts +74 -74
  18. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  19. package/extensions/annotate-git-diff/web/index.html +5 -6
  20. package/extensions/annotate-last-message/README.md +1 -1
  21. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  22. package/extensions/annotate-last-message/index.ts +210 -206
  23. package/extensions/annotate-last-message/package.json +1 -1
  24. package/extensions/annotate-last-message/prompt.ts +87 -66
  25. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  26. package/extensions/annotate-last-message/session.ts +94 -89
  27. package/extensions/annotate-last-message/theme.ts +199 -193
  28. package/extensions/annotate-last-message/types.ts +23 -23
  29. package/extensions/annotate-last-message/ui.ts +27 -25
  30. package/extensions/annotate-last-message/web/app.js +4 -7
  31. package/extensions/annotate-last-message/web/index.html +3 -3
  32. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  33. package/extensions/brrr/README.md +1 -1
  34. package/extensions/brrr/index.ts +213 -191
  35. package/extensions/brrr/package.json +1 -1
  36. package/extensions/claude-fast/README.md +5 -79
  37. package/extensions/claude-fast/index.ts +31 -289
  38. package/extensions/claude-fast/package.json +2 -3
  39. package/extensions/code-reviewer/README.md +6 -6
  40. package/extensions/code-reviewer/index.ts +1825 -1373
  41. package/extensions/code-reviewer/package.json +1 -1
  42. package/extensions/confirm-destructive/README.md +1 -1
  43. package/extensions/confirm-destructive/index.ts +50 -52
  44. package/extensions/confirm-destructive/package.json +1 -1
  45. package/extensions/context-cap/README.md +9 -9
  46. package/extensions/context-cap/index.ts +313 -305
  47. package/extensions/context-cap/package.json +2 -2
  48. package/extensions/context-inspector/index.ts +1211 -1064
  49. package/extensions/context-inspector/package.json +1 -1
  50. package/extensions/contrarian/README.md +4 -3
  51. package/extensions/contrarian/index.ts +1863 -1737
  52. package/extensions/contrarian/package.json +1 -1
  53. package/extensions/dirty-repo-guard/index.ts +37 -41
  54. package/extensions/dirty-repo-guard/package.json +1 -1
  55. package/extensions/dynamic-context-pruning/README.md +11 -43
  56. package/extensions/dynamic-context-pruning/index.ts +2131 -2105
  57. package/extensions/dynamic-context-pruning/package.json +1 -1
  58. package/extensions/fast/README.md +52 -14
  59. package/extensions/fast/index.ts +513 -317
  60. package/extensions/fast/package.json +1 -1
  61. package/extensions/git-footer/README.md +1 -1
  62. package/extensions/git-footer/index.ts +458 -411
  63. package/extensions/git-footer/package.json +2 -3
  64. package/extensions/gnosis/index.ts +180 -130
  65. package/extensions/gnosis/package.json +1 -1
  66. package/extensions/inline-bash/index.ts +87 -103
  67. package/extensions/inline-bash/package.json +1 -1
  68. package/extensions/librarian/README.md +1 -1
  69. package/extensions/librarian/index.ts +1877 -1419
  70. package/extensions/librarian/package.json +1 -1
  71. package/extensions/minimal-footer/README.md +5 -5
  72. package/extensions/minimal-footer/index.ts +987 -1012
  73. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  74. package/extensions/minimal-footer/openai-usage.ts +195 -190
  75. package/extensions/minimal-footer/package.json +1 -1
  76. package/extensions/notify/index.ts +222 -230
  77. package/extensions/notify/package.json +1 -1
  78. package/extensions/openai-fast/README.md +5 -73
  79. package/extensions/openai-fast/index.ts +32 -272
  80. package/extensions/openai-fast/package.json +2 -3
  81. package/extensions/oracle/README.md +4 -3
  82. package/extensions/oracle/index.ts +1740 -1627
  83. package/extensions/oracle/package.json +1 -1
  84. package/extensions/permission-gate/README.md +3 -0
  85. package/extensions/permission-gate/index.ts +785 -612
  86. package/extensions/permission-gate/package.json +1 -1
  87. package/extensions/permission-gate/powershell-safety.ts +840 -793
  88. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  89. package/extensions/project-mcp-json/README.md +199 -0
  90. package/extensions/project-mcp-json/config.ts +564 -0
  91. package/extensions/project-mcp-json/index.ts +119 -0
  92. package/extensions/project-mcp-json/package.json +38 -0
  93. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  94. package/extensions/quiet-tools/README.md +30 -19
  95. package/extensions/quiet-tools/index.ts +376 -291
  96. package/extensions/quiet-tools/package.json +2 -2
  97. package/extensions/review/README.md +2 -2
  98. package/extensions/review/index.ts +1745 -1872
  99. package/extensions/review/package.json +1 -1
  100. package/extensions/todo/index.ts +280 -280
  101. package/extensions/todo/package.json +1 -1
  102. package/extensions/triage-comments/README.md +6 -4
  103. package/extensions/triage-comments/index.ts +2620 -1888
  104. package/extensions/triage-comments/package.json +1 -1
  105. package/package.json +9 -6
  106. package/assets/social-preview.png +0 -0
  107. package/extensions/claude-fast/claude-fast.example.json +0 -4
  108. package/extensions/openai-fast/openai-fast.example.json +0 -4
@@ -7,173 +7,179 @@ import { createInterface } from "node:readline";
7
7
  import type { GlimpseOpenOptions } from "glimpseui";
8
8
 
9
9
  interface NativeHostInfo {
10
- path: string;
11
- extraArgs?: string[];
12
- buildHint?: string;
10
+ path: string;
11
+ extraArgs?: string[];
12
+ buildHint?: string;
13
13
  }
14
14
 
15
15
  interface GlimpseProtocolMessage {
16
- type?: string;
17
- data?: unknown;
18
- screen?: unknown;
19
- screens?: unknown;
20
- appearance?: unknown;
21
- cursor?: unknown;
22
- cursorTip?: unknown;
16
+ type?: string;
17
+ data?: unknown;
18
+ screen?: unknown;
19
+ screens?: unknown;
20
+ appearance?: unknown;
21
+ cursor?: unknown;
22
+ cursorTip?: unknown;
23
23
  }
24
24
 
25
25
  export interface QuietGlimpseWindow {
26
- readonly closed: boolean;
27
- readonly failure: Error | null;
28
- on(event: "message", listener: (data: unknown) => void): this;
29
- on(event: "closed", listener: () => void): this;
30
- on(event: "error", listener: (error: Error) => void): this;
31
- removeListener(event: "message", listener: (data: unknown) => void): this;
32
- removeListener(event: "closed", listener: () => void): this;
33
- removeListener(event: "error", listener: (error: Error) => void): this;
34
- send(js: string): void;
35
- close(): void;
26
+ readonly closed: boolean;
27
+ readonly failure: Error | null;
28
+ on(event: "message", listener: (data: unknown) => void): this;
29
+ on(event: "closed", listener: () => void): this;
30
+ on(event: "error", listener: (error: Error) => void): this;
31
+ removeListener(event: "message", listener: (data: unknown) => void): this;
32
+ removeListener(event: "closed", listener: () => void): this;
33
+ removeListener(event: "error", listener: (error: Error) => void): this;
34
+ send(js: string): void;
35
+ close(): void;
36
36
  }
37
37
 
38
38
  class QuietGlimpseWindowImpl extends EventEmitter implements QuietGlimpseWindow {
39
- #proc: ChildProcessWithoutNullStreams;
40
- #closed = false;
41
- #pendingHTML: string | null;
42
- #stderr = "";
43
- #failure: Error | null = null;
44
-
45
- constructor(proc: ChildProcessWithoutNullStreams, initialHTML: string) {
46
- super();
47
- this.#proc = proc;
48
- this.#pendingHTML = initialHTML;
49
- // EventEmitter treats an unobserved `error` as fatal. Keep a permanent
50
- // fallback while callers attach and detach their lifecycle listeners.
51
- this.on("error", () => {});
52
-
53
- proc.stdin.on("error", () => {});
54
- proc.stderr.on("data", (chunk) => {
55
- this.#stderr += chunk.toString();
56
- });
57
-
58
- const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
59
- rl.on("line", (line) => {
60
- let parsed: unknown;
61
- try {
62
- parsed = JSON.parse(line);
63
- } catch {
64
- this.#reportError(new Error(`Malformed glimpse protocol line: ${line}`));
65
- return;
66
- }
67
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
68
- const message = parsed as GlimpseProtocolMessage;
69
-
70
- switch (message.type) {
71
- case "ready":
72
- if (this.#pendingHTML != null) {
73
- this.setHTML(this.#pendingHTML);
74
- this.#pendingHTML = null;
75
- }
76
- break;
77
- case "message":
78
- this.emit("message", message.data);
79
- break;
80
- case "closed":
81
- this.#markClosed();
82
- break;
83
- default:
84
- break;
85
- }
86
- });
87
-
88
- proc.on("error", (error) => this.#reportError(error));
89
- proc.on("exit", (code, signal) => {
90
- const stderr = this.#stderr.trim();
91
- const failed = signal != null || (code != null && code !== 0);
92
- if (!this.#closed && failed) {
93
- const message = stderr || `Glimpse process exited abnormally (code: ${code}, signal: ${signal}).`;
94
- this.#reportError(new Error(message));
95
- }
96
- this.#markClosed();
97
- });
98
- // Spawn failures can emit `error` and then `close` without `exit`.
99
- proc.on("close", () => this.#markClosed());
100
- }
101
-
102
- send(js: string): void {
103
- this.#write({ type: "eval", js });
104
- }
105
-
106
- close(): void {
107
- this.#write({ type: "close" });
108
- }
109
-
110
- get closed(): boolean {
111
- return this.#closed;
112
- }
113
-
114
- get failure(): Error | null {
115
- return this.#failure;
116
- }
117
-
118
- #reportError(error: Error): void {
119
- this.#failure ??= error;
120
- this.emit("error", error);
121
- }
122
-
123
- #markClosed(): void {
124
- if (this.#closed) return;
125
- this.#closed = true;
126
- this.emit("closed");
127
- }
128
-
129
- #write(obj: Record<string, unknown>): void {
130
- if (this.#closed) return;
131
- this.#proc.stdin.write(`${JSON.stringify(obj)}\n`);
132
- }
133
-
134
- setHTML(html: string): void {
135
- this.#write({ type: "html", html: Buffer.from(html).toString("base64") });
136
- }
39
+ #proc: ChildProcessWithoutNullStreams;
40
+ #closed = false;
41
+ #pendingHTML: string | null;
42
+ #stderr = "";
43
+ #failure: Error | null = null;
44
+
45
+ constructor(proc: ChildProcessWithoutNullStreams, initialHTML: string) {
46
+ super();
47
+ this.#proc = proc;
48
+ this.#pendingHTML = initialHTML;
49
+ // EventEmitter treats an unobserved `error` as fatal. Keep a permanent
50
+ // fallback while callers attach and detach their lifecycle listeners.
51
+ this.on("error", () => {});
52
+
53
+ proc.stdin.on("error", () => {});
54
+ proc.stderr.on("data", (chunk) => {
55
+ this.#stderr += chunk.toString();
56
+ });
57
+
58
+ const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
59
+ rl.on("line", (line) => {
60
+ let parsed: unknown;
61
+ try {
62
+ parsed = JSON.parse(line);
63
+ } catch {
64
+ this.#reportError(new Error(`Malformed glimpse protocol line: ${line}`));
65
+ return;
66
+ }
67
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
68
+ const message = parsed as GlimpseProtocolMessage;
69
+
70
+ switch (message.type) {
71
+ case "ready":
72
+ if (this.#pendingHTML != null) {
73
+ this.setHTML(this.#pendingHTML);
74
+ this.#pendingHTML = null;
75
+ }
76
+ break;
77
+ case "message":
78
+ this.emit("message", message.data);
79
+ break;
80
+ case "closed":
81
+ this.#markClosed();
82
+ break;
83
+ default:
84
+ break;
85
+ }
86
+ });
87
+
88
+ proc.on("error", (error) => this.#reportError(error));
89
+ proc.on("exit", (code, signal) => {
90
+ const stderr = this.#stderr.trim();
91
+ const failed = signal != null || (code != null && code !== 0);
92
+ if (!this.#closed && failed) {
93
+ const message =
94
+ stderr || `Glimpse process exited abnormally (code: ${code}, signal: ${signal}).`;
95
+ this.#reportError(new Error(message));
96
+ }
97
+ this.#markClosed();
98
+ });
99
+ // Spawn failures can emit `error` and then `close` without `exit`.
100
+ proc.on("close", () => this.#markClosed());
101
+ }
102
+
103
+ send(js: string): void {
104
+ this.#write({ type: "eval", js });
105
+ }
106
+
107
+ close(): void {
108
+ this.#write({ type: "close" });
109
+ }
110
+
111
+ get closed(): boolean {
112
+ return this.#closed;
113
+ }
114
+
115
+ get failure(): Error | null {
116
+ return this.#failure;
117
+ }
118
+
119
+ #reportError(error: Error): void {
120
+ this.#failure ??= error;
121
+ this.emit("error", error);
122
+ }
123
+
124
+ #markClosed(): void {
125
+ if (this.#closed) return;
126
+ this.#closed = true;
127
+ this.emit("closed");
128
+ }
129
+
130
+ #write(obj: Record<string, unknown>): void {
131
+ if (this.#closed) return;
132
+ this.#proc.stdin.write(`${JSON.stringify(obj)}\n`);
133
+ }
134
+
135
+ setHTML(html: string): void {
136
+ this.#write({ type: "html", html: Buffer.from(html).toString("base64") });
137
+ }
137
138
  }
138
139
 
139
140
  async function getNativeHostInfo(): Promise<NativeHostInfo> {
140
- const glimpseModule = (await import("glimpseui")) as unknown as { getNativeHostInfo: () => NativeHostInfo };
141
- return glimpseModule.getNativeHostInfo();
141
+ const glimpseModule = (await import("glimpseui")) as unknown as {
142
+ getNativeHostInfo: () => NativeHostInfo;
143
+ };
144
+ return glimpseModule.getNativeHostInfo();
142
145
  }
143
146
 
144
- export async function openQuietGlimpse(html: string, options: GlimpseOpenOptions = {}): Promise<QuietGlimpseWindow> {
145
- const host = await getNativeHostInfo();
146
- if (!existsSync(host.path)) {
147
- const hint = host.buildHint ? ` ${host.buildHint}` : "";
148
- throw new Error(`Glimpse host not found at '${host.path}'.${hint}`);
149
- }
150
- const args: string[] = [];
151
-
152
- if (options.width != null) args.push("--width", String(options.width));
153
- if (options.height != null) args.push("--height", String(options.height));
154
- if (options.title != null) args.push("--title", options.title);
155
- if (options.frameless) args.push("--frameless");
156
- if (options.floating) args.push("--floating");
157
- if (options.transparent) args.push("--transparent");
158
- if (options.clickThrough) args.push("--click-through");
159
- if (options.hidden) args.push("--hidden");
160
- if (options.autoClose) args.push("--auto-close");
161
- if (options.x != null) args.push(`--x=${options.x}`);
162
- if (options.y != null) args.push(`--y=${options.y}`);
163
- if (options.cursorOffset?.x != null) args.push(`--cursor-offset-x=${options.cursorOffset.x}`);
164
- if (options.cursorOffset?.y != null) args.push(`--cursor-offset-y=${options.cursorOffset.y}`);
165
- if (options.cursorAnchor != null) args.push("--cursor-anchor", options.cursorAnchor);
166
- if (options.followMode != null) args.push("--follow-mode", options.followMode);
167
- if (options.followCursor) args.push("--follow-cursor");
168
-
169
- const proc = spawn(host.path, [...(host.extraArgs ?? []), ...args], {
170
- stdio: ["pipe", "pipe", "pipe"],
171
- windowsHide: process.platform === "win32",
172
- env: {
173
- ...process.env,
174
- OS_ACTIVITY_MODE: process.env.OS_ACTIVITY_MODE ?? "disable",
175
- },
176
- });
177
-
178
- return new QuietGlimpseWindowImpl(proc, html);
147
+ export async function openQuietGlimpse(
148
+ html: string,
149
+ options: GlimpseOpenOptions = {},
150
+ ): Promise<QuietGlimpseWindow> {
151
+ const host = await getNativeHostInfo();
152
+ if (!existsSync(host.path)) {
153
+ const hint = host.buildHint ? ` ${host.buildHint}` : "";
154
+ throw new Error(`Glimpse host not found at '${host.path}'.${hint}`);
155
+ }
156
+ const args: string[] = [];
157
+
158
+ if (options.width != null) args.push("--width", String(options.width));
159
+ if (options.height != null) args.push("--height", String(options.height));
160
+ if (options.title != null) args.push("--title", options.title);
161
+ if (options.frameless) args.push("--frameless");
162
+ if (options.floating) args.push("--floating");
163
+ if (options.transparent) args.push("--transparent");
164
+ if (options.clickThrough) args.push("--click-through");
165
+ if (options.hidden) args.push("--hidden");
166
+ if (options.autoClose) args.push("--auto-close");
167
+ if (options.x != null) args.push(`--x=${options.x}`);
168
+ if (options.y != null) args.push(`--y=${options.y}`);
169
+ if (options.cursorOffset?.x != null) args.push(`--cursor-offset-x=${options.cursorOffset.x}`);
170
+ if (options.cursorOffset?.y != null) args.push(`--cursor-offset-y=${options.cursorOffset.y}`);
171
+ if (options.cursorAnchor != null) args.push("--cursor-anchor", options.cursorAnchor);
172
+ if (options.followMode != null) args.push("--follow-mode", options.followMode);
173
+ if (options.followCursor) args.push("--follow-cursor");
174
+
175
+ const proc = spawn(host.path, [...(host.extraArgs ?? []), ...args], {
176
+ stdio: ["pipe", "pipe", "pipe"],
177
+ windowsHide: process.platform === "win32",
178
+ env: {
179
+ ...process.env,
180
+ OS_ACTIVITY_MODE: process.env.OS_ACTIVITY_MODE ?? "disable",
181
+ },
182
+ });
183
+
184
+ return new QuietGlimpseWindowImpl(proc, html);
179
185
  }
@@ -1,112 +1,117 @@
1
1
  import type { SessionEntry, SessionMessageEntry } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import type { LastAssistantMessageData, LastAssistantMessageLookupResult, LastAssistantMessageSection } from "./types.js";
3
+ import type {
4
+ LastAssistantMessageData,
5
+ LastAssistantMessageLookupResult,
6
+ LastAssistantMessageSection,
7
+ } from "./types.js";
4
8
 
5
9
  const SECTION_PREVIEW_LIMIT = 96;
6
10
 
7
11
  function truncatePreview(value: string, limit = SECTION_PREVIEW_LIMIT): string {
8
- const normalized = value.replace(/\s+/g, " ").trim();
9
- if (normalized.length <= limit) return normalized;
10
- return `${normalized.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
12
+ const normalized = value.replace(/\s+/g, " ").trim();
13
+ if (normalized.length <= limit) return normalized;
14
+ return `${normalized.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
11
15
  }
12
16
 
13
17
  function assistantMessageText(
14
- messageEntry: SessionMessageEntry,
18
+ messageEntry: SessionMessageEntry,
15
19
  ): { kind: "assistant"; text: string } | { kind: "incomplete"; stopReason: string } | null {
16
- const { message } = messageEntry;
17
- if (!("role" in message) || message.role !== "assistant") {
18
- return null;
19
- }
20
- if (message.stopReason !== "stop") {
21
- return { kind: "incomplete", stopReason: String(message.stopReason) };
22
- }
23
- if (!Array.isArray(message.content)) {
24
- return { kind: "assistant", text: "" };
25
- }
26
- return {
27
- kind: "assistant",
28
- text: message.content
29
- .filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string")
30
- .map((part) => part.text)
31
- .join("\n")
32
- .replace(/\r\n?/g, "\n"),
33
- };
20
+ const { message } = messageEntry;
21
+ if (!("role" in message) || message.role !== "assistant") {
22
+ return null;
23
+ }
24
+ if (message.stopReason !== "stop") {
25
+ return { kind: "incomplete", stopReason: String(message.stopReason) };
26
+ }
27
+ if (!Array.isArray(message.content)) {
28
+ return { kind: "assistant", text: "" };
29
+ }
30
+ return {
31
+ kind: "assistant",
32
+ text: message.content
33
+ .filter(
34
+ (part): part is { type: "text"; text: string } =>
35
+ part.type === "text" && typeof part.text === "string",
36
+ )
37
+ .map((part) => part.text)
38
+ .join("\n")
39
+ .replace(/\r\n?/g, "\n"),
40
+ };
34
41
  }
35
42
 
36
43
  function buildSections(lines: string[]): LastAssistantMessageSection[] {
37
- const sections: LastAssistantMessageSection[] = [];
38
- let startIndex: number | null = null;
44
+ const sections: LastAssistantMessageSection[] = [];
45
+ let startIndex: number | null = null;
39
46
 
40
- const flushSection = (endIndex: number): void => {
41
- if (startIndex == null) return;
42
- const sectionLines = lines.slice(startIndex, endIndex + 1);
43
- const previewSource = sectionLines.find((line) => line.trim().length > 0) ?? "";
44
- sections.push({
45
- id: `section-${sections.length + 1}`,
46
- index: sections.length + 1,
47
- startLine: startIndex + 1,
48
- endLine: endIndex + 1,
49
- preview: truncatePreview(previewSource),
50
- text: sectionLines.join("\n"),
51
- });
52
- startIndex = null;
53
- };
47
+ const flushSection = (endIndex: number): void => {
48
+ if (startIndex == null) return;
49
+ const sectionLines = lines.slice(startIndex, endIndex + 1);
50
+ const previewSource = sectionLines.find((line) => line.trim().length > 0) ?? "";
51
+ sections.push({
52
+ id: `section-${sections.length + 1}`,
53
+ index: sections.length + 1,
54
+ startLine: startIndex + 1,
55
+ endLine: endIndex + 1,
56
+ preview: truncatePreview(previewSource),
57
+ });
58
+ startIndex = null;
59
+ };
54
60
 
55
- for (let index = 0; index < lines.length; index += 1) {
56
- const line = lines[index];
57
- if (line.trim().length === 0) {
58
- flushSection(index - 1);
59
- continue;
60
- }
61
- if (startIndex == null) {
62
- startIndex = index;
63
- }
64
- }
61
+ for (let index = 0; index < lines.length; index += 1) {
62
+ const line = lines[index];
63
+ if (line.trim().length === 0) {
64
+ flushSection(index - 1);
65
+ continue;
66
+ }
67
+ if (startIndex == null) {
68
+ startIndex = index;
69
+ }
70
+ }
65
71
 
66
- flushSection(lines.length - 1);
67
- return sections;
72
+ flushSection(lines.length - 1);
73
+ return sections;
68
74
  }
69
75
 
70
76
  export function findLastAssistantMessage(branch: SessionEntry[]): LastAssistantMessageLookupResult {
71
- for (let index = branch.length - 1; index >= 0; index -= 1) {
72
- const entry = branch[index];
73
- if (entry.type !== "message") {
74
- continue;
75
- }
76
- const extracted = assistantMessageText(entry);
77
- if (extracted == null) {
78
- continue;
79
- }
80
- if (extracted.kind === "incomplete") {
81
- return {
82
- ok: false,
83
- code: "incomplete",
84
- message: `Latest assistant message is incomplete (${extracted.stopReason}). Wait for it to finish, then try again.`,
85
- };
86
- }
77
+ for (let index = branch.length - 1; index >= 0; index -= 1) {
78
+ const entry = branch[index];
79
+ if (entry.type !== "message") {
80
+ continue;
81
+ }
82
+ const extracted = assistantMessageText(entry);
83
+ if (extracted == null) {
84
+ continue;
85
+ }
86
+ if (extracted.kind === "incomplete") {
87
+ return {
88
+ ok: false,
89
+ code: "incomplete",
90
+ message: `Latest assistant message is incomplete (${extracted.stopReason}). Wait for it to finish, then try again.`,
91
+ };
92
+ }
87
93
 
88
- const text = extracted.text;
89
- const trimmed = text.trim();
90
- if (trimmed.length === 0) {
91
- return {
92
- ok: false,
93
- code: "empty",
94
- message: "Latest assistant message has no text to annotate.",
95
- };
96
- }
94
+ const text = extracted.text;
95
+ const trimmed = text.trim();
96
+ if (trimmed.length === 0) {
97
+ return {
98
+ ok: false,
99
+ code: "empty",
100
+ message: "Latest assistant message has no text to annotate.",
101
+ };
102
+ }
97
103
 
98
- const lines = text.split("\n");
99
- const data: LastAssistantMessageData = {
100
- text,
101
- lines: lines.map((line, lineIndex) => ({ number: lineIndex + 1, text: line })),
102
- sections: buildSections(lines),
103
- };
104
- return { ok: true, data };
105
- }
104
+ const lines = text.split("\n");
105
+ const data: LastAssistantMessageData = {
106
+ lines: lines.map((line, lineIndex) => ({ number: lineIndex + 1, text: line })),
107
+ sections: buildSections(lines),
108
+ };
109
+ return { ok: true, data };
110
+ }
106
111
 
107
- return {
108
- ok: false,
109
- code: "missing",
110
- message: "No assistant messages found on the current session branch.",
111
- };
112
+ return {
113
+ ok: false,
114
+ code: "missing",
115
+ message: "No assistant messages found on the current session branch.",
116
+ };
112
117
  }