@diegopetrucci/pi-extensions 0.1.74 → 0.1.76

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 (93) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +9 -4
  3. package/extensions/agent-workflow-audit/index.ts +1398 -1009
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/README.md +3 -1
  6. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  7. package/extensions/annotate-git-diff/git.ts +853 -767
  8. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  9. package/extensions/annotate-git-diff/index.ts +712 -671
  10. package/extensions/annotate-git-diff/package.json +1 -1
  11. package/extensions/annotate-git-diff/prompt.ts +45 -45
  12. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  13. package/extensions/annotate-git-diff/review-server.ts +337 -311
  14. package/extensions/annotate-git-diff/types.ts +132 -132
  15. package/extensions/annotate-git-diff/ui.ts +27 -27
  16. package/extensions/annotate-git-diff/watch.ts +74 -74
  17. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  18. package/extensions/annotate-git-diff/web/index.html +5 -6
  19. package/extensions/annotate-last-message/README.md +1 -1
  20. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  21. package/extensions/annotate-last-message/index.ts +210 -206
  22. package/extensions/annotate-last-message/package.json +1 -1
  23. package/extensions/annotate-last-message/prompt.ts +87 -66
  24. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  25. package/extensions/annotate-last-message/session.ts +94 -89
  26. package/extensions/annotate-last-message/theme.ts +199 -193
  27. package/extensions/annotate-last-message/types.ts +23 -23
  28. package/extensions/annotate-last-message/ui.ts +27 -25
  29. package/extensions/annotate-last-message/web/app.js +4 -7
  30. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  31. package/extensions/brrr/README.md +1 -1
  32. package/extensions/brrr/index.ts +213 -191
  33. package/extensions/brrr/package.json +1 -1
  34. package/extensions/claude-fast/README.md +5 -79
  35. package/extensions/claude-fast/index.ts +31 -289
  36. package/extensions/claude-fast/package.json +2 -3
  37. package/extensions/code-reviewer/README.md +1 -1
  38. package/extensions/code-reviewer/index.ts +1598 -1350
  39. package/extensions/code-reviewer/package.json +1 -1
  40. package/extensions/confirm-destructive/README.md +1 -1
  41. package/extensions/confirm-destructive/index.ts +55 -51
  42. package/extensions/confirm-destructive/package.json +1 -1
  43. package/extensions/context-cap/index.ts +312 -304
  44. package/extensions/context-cap/package.json +1 -1
  45. package/extensions/context-inspector/index.ts +1211 -1064
  46. package/extensions/context-inspector/package.json +1 -1
  47. package/extensions/contrarian/index.ts +1854 -1735
  48. package/extensions/contrarian/package.json +1 -1
  49. package/extensions/dirty-repo-guard/index.ts +41 -41
  50. package/extensions/dynamic-context-pruning/README.md +6 -38
  51. package/extensions/dynamic-context-pruning/index.ts +2124 -1980
  52. package/extensions/dynamic-context-pruning/package.json +1 -1
  53. package/extensions/fast/README.md +50 -12
  54. package/extensions/fast/index.ts +513 -317
  55. package/extensions/fast/package.json +1 -1
  56. package/extensions/git-footer/README.md +1 -1
  57. package/extensions/git-footer/index.ts +458 -411
  58. package/extensions/git-footer/package.json +2 -3
  59. package/extensions/gnosis/index.ts +180 -130
  60. package/extensions/gnosis/package.json +1 -1
  61. package/extensions/inline-bash/index.ts +88 -88
  62. package/extensions/inline-bash/package.json +1 -1
  63. package/extensions/librarian/index.ts +1669 -1419
  64. package/extensions/librarian/package.json +1 -1
  65. package/extensions/minimal-footer/index.ts +994 -1012
  66. package/extensions/minimal-footer/openai-usage.ts +195 -190
  67. package/extensions/notify/index.ts +222 -230
  68. package/extensions/notify/package.json +1 -1
  69. package/extensions/openai-fast/README.md +5 -73
  70. package/extensions/openai-fast/index.ts +32 -272
  71. package/extensions/openai-fast/package.json +2 -3
  72. package/extensions/oracle/index.ts +1731 -1625
  73. package/extensions/oracle/package.json +1 -1
  74. package/extensions/permission-gate/index.ts +658 -608
  75. package/extensions/permission-gate/package.json +1 -1
  76. package/extensions/permission-gate/powershell-safety.ts +840 -793
  77. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  78. package/extensions/project-mcp-json/README.md +199 -0
  79. package/extensions/project-mcp-json/config.ts +564 -0
  80. package/extensions/project-mcp-json/index.ts +119 -0
  81. package/extensions/project-mcp-json/package.json +38 -0
  82. package/extensions/quiet-tools/index.ts +282 -264
  83. package/extensions/review/README.md +2 -2
  84. package/extensions/review/index.ts +1745 -1872
  85. package/extensions/review/package.json +1 -1
  86. package/extensions/todo/index.ts +284 -265
  87. package/extensions/triage-comments/README.md +2 -2
  88. package/extensions/triage-comments/index.ts +2294 -1882
  89. package/extensions/triage-comments/package.json +1 -1
  90. package/package.json +6 -3
  91. package/assets/social-preview.png +0 -0
  92. package/extensions/claude-fast/claude-fast.example.json +0 -4
  93. 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
  }