@lincoln504/pi-research 0.1.12 → 1.0.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 (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -115
  3. package/dist/cli.mjs +23211 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1044 -0
  30. package/src/config.ts +1036 -147
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +264 -7
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -174
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -577
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Research TUI Manager
3
+ *
4
+ * Manages TUI interactions for research sessions:
5
+ * - Master widget creation and updates
6
+ * - Session panel management
7
+ * - Terminal input handling for cancellation
8
+ * - Debounced refresh coordination
9
+ */
10
+
11
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
12
+ import type { TUI } from '@earendil-works/pi-tui';
13
+ import { logger } from '../logger.ts';
14
+ import type { Theme } from '../types/research-panel-types.ts';
15
+ import {
16
+ createMasterResearchPanel,
17
+ createInitialPanelState,
18
+ clearAllFlashTimeouts,
19
+ } from './research-panel.ts';
20
+ import {
21
+ registerSessionPanel,
22
+ registerMasterUpdate,
23
+ registerMasterRemove,
24
+ refreshAllSessions,
25
+ flushMasterNow,
26
+ onSessionOrderChange,
27
+ getPiActivePanels,
28
+ normalizeSessionId,
29
+ } from '../orchestration/session-state.ts';
30
+ import { initGlobalTuiController, isInteractiveTuiActive } from './tui-controller.ts';
31
+ import type { ResearchPanelState } from './research-panel.ts';
32
+
33
+ export interface TuiContext {
34
+ piSessionId: string;
35
+ researchId: string;
36
+ query: string;
37
+ modelId: string;
38
+ }
39
+
40
+ export interface TuiDependencies {
41
+ ctx: ExtensionContext;
42
+ }
43
+
44
+ export interface TuiManager {
45
+ panelState: ResearchPanelState;
46
+ masterWidgetId: string;
47
+ unsubOrder: (() => void) | null;
48
+ debouncedRefresh: () => void;
49
+ /**
50
+ * Render the panel immediately, bypassing the debounce. Used by the wave
51
+ * animation pulse so frames are not swallowed by the 100ms state-refresh
52
+ * debounce; pi-tui's own 16ms throttle + diff caps and smooths the output.
53
+ */
54
+ renderImmediate: () => void;
55
+ initializePanel: () => void;
56
+ dispose: () => void;
57
+ }
58
+
59
+ /**
60
+ * Create a TUI manager for a research session
61
+ */
62
+ export function createResearchTuiManager(
63
+ tuiCtx: TuiContext,
64
+ deps: TuiDependencies
65
+ ): TuiManager {
66
+ const {
67
+ piSessionId: rawPiSessionId,
68
+ researchId,
69
+ query,
70
+ modelId,
71
+ } = tuiCtx;
72
+
73
+ const piSessionId = normalizeSessionId(rawPiSessionId);
74
+ const { ctx } = deps;
75
+
76
+ const masterWidgetId = `pi-research-master-${piSessionId}`;
77
+ const panelState = createInitialPanelState(piSessionId, researchId, query, modelId);
78
+ logger.debug(`[tui-manager] Creating manager for session ${piSessionId}, research ${researchId}`);
79
+
80
+ // Ensure the global TUI controller is initialized (handles cancellation and protocol leaks)
81
+ // Only in TUI mode with UI available
82
+ if (ctx.mode === 'tui' && ctx.hasUI) {
83
+ initGlobalTuiController(ctx.ui, piSessionId);
84
+ }
85
+
86
+ let unsubOrder: (() => void) | null = null;
87
+ let refreshTimeout: NodeJS.Timeout | null = null;
88
+ let refreshScheduled = false;
89
+
90
+ // Register the panel state
91
+ registerSessionPanel(piSessionId, researchId, panelState);
92
+
93
+ /**
94
+ * Debounced refresh to avoid excessive UI updates
95
+ */
96
+ const debouncedRefresh = () => {
97
+ // Only refresh if in TUI mode
98
+ if (ctx.mode !== 'tui') return;
99
+
100
+ if (refreshScheduled) return;
101
+
102
+ // Do not schedule background refreshes if a foreground interactive menu is active.
103
+ // This prevents background processing from stealing focus or fighting with the
104
+ // interactive TUI's own rendering loop.
105
+ if (isInteractiveTuiActive()) return;
106
+
107
+ refreshScheduled = true;
108
+
109
+ refreshTimeout = setTimeout(() => {
110
+ refreshScheduled = false;
111
+ refreshAllSessions(piSessionId);
112
+ }, 16); // 16ms coalescing window for low-frequency state changes (tokens,
113
+ // status, slices). Animation frames do NOT use this path — they go
114
+ // through renderImmediate() so they are never swallowed by the
115
+ // downstream 100ms refreshAllSessions debounce.
116
+ };
117
+
118
+ /**
119
+ * Initialize the master widget
120
+ */
121
+ const initializePanel = () => {
122
+ // Only initialize if in TUI mode and UI is available
123
+ if (ctx.mode !== 'tui' || !ctx.hasUI) return;
124
+
125
+ // Do not initialize background widgets if a foreground interactive menu is active
126
+ if (isInteractiveTuiActive()) return;
127
+
128
+ const masterPanelCreator = createMasterResearchPanel(piSessionId, () => {
129
+ // Get active panels lazily to avoid circular dependencies
130
+ return getActivePanelsForSession(piSessionId);
131
+ });
132
+
133
+ ctx.ui.setWidget(masterWidgetId, (tui: TUI, theme: Theme) => masterPanelCreator(tui, theme), { placement: 'aboveEditor' });
134
+ };
135
+
136
+ // Register master update function (re-registers the widget to trigger a render)
137
+ // setExtensionWidget() removes the old widget before adding the new one — no stacking.
138
+ // Calling refreshAllSessions() here would cause an infinite loop:
139
+ // debouncedRefresh → refreshAllSessions → masterUpdate → refreshAllSessions → …
140
+ registerMasterUpdate(piSessionId, () => {
141
+ // Only update if in TUI mode and UI is available
142
+ if (ctx.mode !== 'tui' || !ctx.hasUI) return;
143
+
144
+ // Do not update background widgets if a foreground interactive menu is active!
145
+ // This prevents the background research widget from fighting with the settings menu,
146
+ // which causes the terminal cursor to jump and "spam" duplicates down the screen.
147
+ if (isInteractiveTuiActive()) return;
148
+
149
+ const masterPanelCreator = createMasterResearchPanel(piSessionId, () => {
150
+ return getActivePanelsForSession(piSessionId);
151
+ });
152
+ ctx.ui.setWidget(masterWidgetId, (tui: TUI, theme: Theme) => masterPanelCreator(tui, theme), { placement: 'aboveEditor' });
153
+ });
154
+
155
+ // Register the remover so a foreground menu can fully hide the live panel.
156
+ // setWidget(key, undefined) removes the widget and forces a clean re-layout
157
+ // (renderWidgets()), which both frees the screen for the menu and clears any
158
+ // rows the panel occupied — preventing ghost/stacked panels on close.
159
+ registerMasterRemove(piSessionId, () => {
160
+ if (ctx.mode !== 'tui' || !ctx.hasUI) return;
161
+ (ctx.ui as unknown as { setWidget: (k: string, c?: unknown) => void }).setWidget(masterWidgetId, undefined);
162
+ });
163
+
164
+ // Subscribe to session order changes
165
+ unsubOrder = onSessionOrderChange(piSessionId, () => refreshAllSessions(piSessionId));
166
+
167
+ /**
168
+ * Dispose of TUI resources
169
+ */
170
+ const dispose = () => {
171
+ // Scope the clear to THIS run — clearing by piSessionId would cancel the flash
172
+ // timers of any concurrent sibling run sharing the session.
173
+ clearAllFlashTimeouts(researchId);
174
+
175
+ if (refreshTimeout) {
176
+ clearTimeout(refreshTimeout);
177
+ refreshTimeout = null;
178
+ }
179
+
180
+ if (unsubOrder) {
181
+ unsubOrder();
182
+ unsubOrder = null;
183
+ }
184
+ };
185
+
186
+ /**
187
+ * Immediate (non-debounced) render for animation frames.
188
+ * Honors the same TUI-mode guard as debouncedRefresh; the foreground-menu
189
+ * guard lives inside masterUpdate (flushMasterNow → masterUpdate).
190
+ */
191
+ const renderImmediate = () => {
192
+ if (ctx.mode !== 'tui') return;
193
+ flushMasterNow(piSessionId);
194
+ };
195
+
196
+ return {
197
+ panelState,
198
+ masterWidgetId,
199
+ unsubOrder,
200
+ debouncedRefresh,
201
+ renderImmediate,
202
+ initializePanel,
203
+ dispose,
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Get active panels for a session
209
+ * Helper function to avoid circular dependencies
210
+ */
211
+ function getActivePanelsForSession(piSessionId: string) {
212
+ return getPiActivePanels(piSessionId);
213
+ }
214
+
215
+ /**
216
+ * Hide the working indicator during research
217
+ */
218
+ export function hideWorkingIndicator(ctx: ExtensionContext): void {
219
+ const tuiUI = ctx.ui as { setWorkingVisible?: (visible: boolean) => void };
220
+ if (typeof tuiUI?.setWorkingVisible === 'function') {
221
+ tuiUI.setWorkingVisible(false);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Show the working indicator (typically on cleanup)
227
+ */
228
+ export function showWorkingIndicator(ctx: ExtensionContext): void {
229
+ const tuiUI = ctx.ui as { setWorkingVisible?: (visible: boolean) => void };
230
+ if (typeof tuiUI?.setWorkingVisible === 'function') {
231
+ tuiUI.setWorkingVisible(true);
232
+ }
233
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Global TUI Controller
3
+ *
4
+ * Centralizes TUI-related logic that needs to be global across the extension:
5
+ * - Single onTerminalInput listener per Pi session
6
+ * - Protocol response filtering
7
+ * - Global cancellation (Esc/Ctrl+C) with context awareness
8
+ */
9
+
10
+ import type { ExtensionUIContext } from '@earendil-works/pi-coding-agent';
11
+ import { matchesKey } from '@earendil-works/pi-tui';
12
+ import { getActiveSessionCount, abortAllSessions, refreshAllSessions, hideMasterWidget } from '../orchestration/session-state.ts';
13
+ import { logger } from '../logger.ts';
14
+
15
+ /**
16
+ * Global state for TUI controller
17
+ */
18
+ interface GlobalTuiState {
19
+ /** Unsubscribe function for terminal input listener */
20
+ unsubInput: (() => void) | null;
21
+ /** Whether an interactive custom TUI is currently active */
22
+ isInteractiveTuiActive: boolean;
23
+ /** The Pi session ID this controller belongs to */
24
+ piSessionId: string | undefined;
25
+ }
26
+
27
+ const state: GlobalTuiState = {
28
+ unsubInput: null,
29
+ isInteractiveTuiActive: false,
30
+ piSessionId: undefined,
31
+ };
32
+
33
+ /**
34
+ * Initialize the global TUI controller
35
+ *
36
+ * @param ui - The UI context from Pi
37
+ * @param piSessionId - The current Pi session ID for scoped cancellation
38
+ */
39
+ export function initGlobalTuiController(ui: ExtensionUIContext, piSessionId?: string): void {
40
+ // Always refresh the active session id FIRST — even when the input handler is
41
+ // already wired from a prior run. A second research run in the same process must
42
+ // scope cancellation (Esc/Ctrl+C), widget hide/restore, and abortAllSessions to
43
+ // ITS session; the old guard returned early and left piSessionId stuck on run 1.
44
+ state.piSessionId = piSessionId;
45
+
46
+ if (state.unsubInput) return;
47
+
48
+ // Double check if ui and onTerminalInput exist (it should in interactive mode)
49
+ if (!ui || typeof ui.onTerminalInput !== 'function') {
50
+ logger.debug('[TUI] Global TUI controller skipped: UI context or onTerminalInput not available');
51
+ return;
52
+ }
53
+
54
+ /**
55
+ * Handle terminal input for cancellation and protocol cleanup
56
+
57
+ */
58
+ const handleTerminalInput = (data: string) => {
59
+ // If an interactive TUI is active (like the config menu),
60
+ // let it handle ALL input. We don't want to trigger a global abort
61
+ // while the user is navigating a menu.
62
+ if (state.isInteractiveTuiActive) {
63
+ return undefined;
64
+ }
65
+
66
+ // Check for specific global cancel keys (Escape and Ctrl+C)
67
+ // We only trigger abort if research is actually running.
68
+ if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {
69
+ const activeCount = getActiveSessionCount();
70
+ if (activeCount > 0) {
71
+ // FIX (New Issues B & E): Use abortAllSessions scoped to the current Pi session
72
+ // instead of clearAllSessionState() which nukes ALL sessions across ALL Pi instances.
73
+ logger.info(`[TUI] Global cancel requested (${data === '\x03' ? 'Ctrl+C' : 'Esc'}). Aborting active sessions...`);
74
+
75
+ try {
76
+ // Scope to this Pi session to avoid cross-session interference
77
+ abortAllSessions(state.piSessionId);
78
+ } catch (err) {
79
+ logger.error('[TUI] Failed to abort sessions on cancel:', err);
80
+ }
81
+ }
82
+
83
+ // Let the key fall through so Pi can handle its own cancellation
84
+ // or other TUIs can respond.
85
+ return undefined;
86
+ }
87
+
88
+ return undefined;
89
+ };
90
+
91
+ // Register the global listener
92
+ state.unsubInput = ui.onTerminalInput(handleTerminalInput);
93
+
94
+ logger.debug('[TUI] Global TUI controller initialized');
95
+ }
96
+
97
+ /**
98
+ * Mark an interactive TUI as active/inactive
99
+ */
100
+ export function setInteractiveTuiActive(active: boolean): void {
101
+ state.isInteractiveTuiActive = active;
102
+ logger.debug(`[TUI] Interactive TUI state changed: ${active}`);
103
+
104
+ if (active && state.piSessionId !== undefined) {
105
+ // When a foreground menu opens, fully remove the live research panel from
106
+ // the screen. Freezing its updates (the isInteractiveTuiActive gate) is not
107
+ // enough: a tall, still-present panel above the editor crowds the inline
108
+ // menu and leaves ghost rows behind. Removing it gives the menu the full
109
+ // screen, identical to opening the menu with no run active.
110
+ hideMasterWidget(state.piSessionId);
111
+ }
112
+
113
+ if (!active && state.piSessionId !== undefined) {
114
+ // When the menu closes, immediately refresh the background research widgets
115
+ // because they were paused/removed while the menu was open.
116
+ refreshAllSessions(state.piSessionId);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Check if an interactive TUI is active
122
+ */
123
+ export function isInteractiveTuiActive(): boolean {
124
+ return state.isInteractiveTuiActive;
125
+ }
126
+
127
+ /**
128
+ * Dispose the global TUI controller
129
+ */
130
+ export function disposeGlobalTuiController(): void {
131
+ if (state.unsubInput) {
132
+ state.unsubInput();
133
+ state.unsubInput = null;
134
+ }
135
+ state.piSessionId = undefined;
136
+ state.isInteractiveTuiActive = false;
137
+ }
@@ -1,60 +1,29 @@
1
1
  /**
2
2
  * Extended Type Definitions for pi Extension Context
3
3
  *
4
- * These types extend the base ExtensionContext from @mariozechner/pi-coding-agent
4
+ * These types extend the base ExtensionContext from @earendil-works/pi-coding-agent
5
5
  * to provide type safety for additional properties that are accessed at runtime.
6
6
  */
7
7
 
8
- import type { SettingsManager } from '@mariozechner/pi-coding-agent';
8
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
9
+ export type { AgentSessionEvent } from '@earendil-works/pi-coding-agent';
9
10
 
10
11
  /**
11
- * Session manager interface for accessing session metadata
12
- */
13
- export interface SessionManager {
14
- getSessionId(): string;
15
- getSessionFile(): string;
16
- }
17
-
18
- /**
19
- * Extended ExtensionContext with additional properties
20
- * Note: We don't extend ExtensionContext to avoid type conflicts with
21
- * ReadonlySessionManager. This is used as a cast target only.
12
+ * Extended ExtensionContext with additional properties.
13
+ * Provides type safety for properties accessed at runtime.
14
+ *
15
+ * NOTE: We do NOT re-declare properties from ExtensionContext (sessionManager,
16
+ * modelRegistry, etc.) to avoid type conflicts when the base library evolves.
17
+ * Instead, consumers cast to ExtendedExtensionContext only for the properties
18
+ * that the base type doesn't include (sessionId, settingsManager, excludeTools).
22
19
  */
23
- export interface ExtendedExtensionContext {
24
- sessionManager?: SessionManager;
25
- settingsManager?: SettingsManager;
26
- }
20
+ export interface ExtendedExtensionContext extends ExtensionContext {
21
+ /** Session ID (provided by extension host) */
22
+ sessionId?: string;
23
+ /** Settings manager (provided in some contexts) */
24
+ settingsManager?: any;
25
+ /** Current tool exclusion list */
26
+ excludeTools?: string[];
27
+ }
27
28
 
28
- /**
29
- * Model with ID property and optional cost information
30
- */
31
- export interface ModelWithId {
32
- id: string;
33
- cost?: {
34
- input: number;
35
- output: number;
36
- cacheRead?: number;
37
- cacheWrite?: number;
38
- };
39
- }
40
29
 
41
- /**
42
- * Agent session event with usage and error information
43
- */
44
- export interface ExtendedAgentSessionEvent {
45
- type: string;
46
- message?: {
47
- role?: string;
48
- usage?: {
49
- totalTokens?: number;
50
- input?: number;
51
- output?: number;
52
- cacheRead?: number;
53
- cacheWrite?: number;
54
- };
55
- };
56
- isError?: boolean;
57
- details?: {
58
- blocked?: boolean;
59
- };
60
- }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Central type definitions for pi-research
3
+ * This module provides strongly-typed interfaces to replace 'as any' usage
4
+ */
5
+
6
+ /**
7
+ * Research depth levels
8
+ */
9
+ export type ResearchDepth = 0 | 1 | 2 | 3;
10
+
11
+ /**
12
+ * Research result details
13
+ */
14
+ export interface ResearchResultDetails {
15
+ totalTokens?: number;
16
+ cost?: number;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ /**
21
+ * Browser task with query or URL
22
+ */
23
+ export interface BrowserTask {
24
+ query?: string;
25
+ url?: string;
26
+ sessionId?: string;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ /**
31
+ * Context object threaded through cleanup operations for a research session.
32
+ */
33
+ export interface CleanupContext {
34
+ researchId: string;
35
+ piSessionId: string;
36
+ masterWidgetId: string;
37
+ panelState: import('./research-panel-types.ts').ResearchPanelState;
38
+ waveTimer: NodeJS.Timeout | null;
39
+ unsubOrder: (() => void) | null;
40
+ unsubOrderRef?: { value: (() => void) | null };
41
+ }
42
+
43
+ /**
44
+ * Research message from observer events
45
+ */
46
+ export interface ResearchMessage {
47
+ type: string;
48
+ content?: string;
49
+ [key: string]: unknown;
50
+ }
51
+
52
+ /**
53
+ * Node.js error with code property
54
+ */
55
+ export interface NodeError extends Error {
56
+ code?: string;
57
+ errno?: number;
58
+ syscall?: string;
59
+ [key: string]: unknown;
60
+ }
61
+
package/src/types/llm.ts CHANGED
@@ -2,12 +2,14 @@
2
2
  * LLM-Related Type Definitions
3
3
  *
4
4
  * Shared type definitions for LLM responses, usage, and message content.
5
- * These types complement the types from @mariozechner/pi-ai and provide
5
+ * These types complement the types from @earendil-works/pi-ai and provide
6
6
  * more specific typing for our use cases.
7
7
  */
8
8
 
9
+ import { calculateCost, type Model } from '@earendil-works/pi-ai';
10
+
9
11
  /**
10
- * Token usage information from LLM responses
12
+ * Token usage and cost information from LLM responses
11
13
  */
12
14
  export interface TokenUsage {
13
15
  /** Input tokens consumed */
@@ -20,10 +22,18 @@ export interface TokenUsage {
20
22
  cacheWrite?: number;
21
23
  /** Total tokens (may be provided by some providers) */
22
24
  totalTokens?: number;
25
+ /** Cost details */
26
+ cost?: {
27
+ input: number;
28
+ output: number;
29
+ cacheRead: number;
30
+ cacheWrite: number;
31
+ total: number;
32
+ };
23
33
  }
24
34
 
25
35
  /**
26
- * Text content block in a message
36
+ * Text content block
27
37
  */
28
38
  export interface TextContentBlock {
29
39
  type: 'text';
@@ -31,48 +41,44 @@ export interface TextContentBlock {
31
41
  }
32
42
 
33
43
  /**
34
- * Message content (can be a string or array of blocks)
44
+ * Thinking/Reasoning content block
35
45
  */
36
- export type MessageContent = string | TextContentBlock[];
46
+ export interface ThinkingContentBlock {
47
+ type: 'thinking';
48
+ content: string;
49
+ }
37
50
 
38
51
  /**
39
- * Filter for text content blocks from a message content array
52
+ * Tool call block
40
53
  */
41
- export function isTextContentBlock(block: unknown): block is TextContentBlock {
42
- return (
43
- block !== null &&
44
- typeof block === 'object' &&
45
- (block as Record<string, unknown>)['type'] === 'text' &&
46
- typeof (block as Record<string, unknown>)['text'] === 'string'
47
- );
54
+ export interface ToolCallBlock {
55
+ type: 'tool_call';
56
+ id?: string;
57
+ name: string;
58
+ arguments: any;
48
59
  }
49
60
 
50
61
  /**
51
- * Extract text from a message content block or string
62
+ * Message content block
52
63
  */
53
- export function extractTextFromContent(content: MessageContent): string {
54
- if (typeof content === 'string') {
55
- return content;
56
- }
57
- if (Array.isArray(content)) {
58
- return content
59
- .filter(isTextContentBlock)
60
- .map(block => block.text)
61
- .join('\n');
62
- }
63
- return '';
64
- }
64
+ export type MessageBlock = TextContentBlock | ThinkingContentBlock | ToolCallBlock;
65
+
66
+ /**
67
+ * Message content (can be a string or array of blocks)
68
+ */
69
+ export type MessageContent = string | MessageBlock[];
70
+
65
71
 
66
72
  /**
67
73
  * Parse token usage from an unknown object
68
74
  * Returns a partial usage object with any fields that were present
69
75
  */
70
- export function parseTokenUsage(usage: unknown): Partial<TokenUsage> {
76
+ function parseTokenUsage(usage: unknown): Partial<TokenUsage> {
71
77
  if (!usage || typeof usage !== 'object') {
72
78
  return {};
73
79
  }
74
80
 
75
- const obj = usage as Record<string, unknown>;
81
+ const obj = usage as Record<string, any>;
76
82
  const result: Partial<TokenUsage> = {};
77
83
 
78
84
  if (typeof obj['input'] === 'number') {
@@ -90,6 +96,9 @@ export function parseTokenUsage(usage: unknown): Partial<TokenUsage> {
90
96
  if (typeof obj['totalTokens'] === 'number') {
91
97
  result.totalTokens = obj['totalTokens'];
92
98
  }
99
+ if (obj['cost'] && typeof obj['cost'] === 'object') {
100
+ result.cost = obj['cost'];
101
+ }
93
102
 
94
103
  return result;
95
104
  }
@@ -98,7 +107,7 @@ export function parseTokenUsage(usage: unknown): Partial<TokenUsage> {
98
107
  * Calculate total tokens from usage object
99
108
  * Falls back to sum of individual components if totalTokens not provided
100
109
  */
101
- export function calculateTotalTokens(usage: Partial<TokenUsage>): number {
110
+ function calculateTotalTokens(usage: Partial<TokenUsage>): number {
102
111
  if (usage.totalTokens !== undefined) {
103
112
  return usage.totalTokens;
104
113
  }
@@ -109,3 +118,33 @@ export function calculateTotalTokens(usage: Partial<TokenUsage>): number {
109
118
  (usage.cacheWrite ?? 0)
110
119
  );
111
120
  }
121
+
122
+ /**
123
+ * Extract usage (tokens and cost) from a raw LLM usage object.
124
+ * Handles both provided cost and estimated cost via calculateCost.
125
+ *
126
+ * @param model - The model used for the call (for cost estimation)
127
+ * @param rawUsage - The raw usage object from the LLM response
128
+ */
129
+ export function extractUsage(model: Model<any>, rawUsage: any): { tokens: number; cost: number; parsed: Partial<TokenUsage> } {
130
+ if (!rawUsage) {
131
+ return { tokens: 0, cost: 0, parsed: {} };
132
+ }
133
+
134
+ const parsed = parseTokenUsage(rawUsage);
135
+ const tokens = calculateTotalTokens(parsed);
136
+
137
+ // Ultra-accurate cost calculation
138
+ let cost = parsed.cost?.total ?? rawUsage.cost?.total ?? 0;
139
+ if (cost === 0 && tokens > 0) {
140
+ try {
141
+ const calculatedCost = calculateCost(model, rawUsage);
142
+ cost = calculatedCost.total;
143
+ } catch {
144
+ // Best-effort cost calculation; if calculateCost fails (e.g. model not in registry), keep 0.
145
+ }
146
+ }
147
+
148
+ return { tokens, cost, parsed };
149
+ }
150
+