@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.
- package/LICENSE +1 -1
- package/README.md +64 -115
- package/dist/cli.mjs +23211 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +102 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +213 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +113 -0
- package/docs/THIRD-PARTY-NOTICES.md +64 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1044 -0
- package/src/config.ts +1036 -147
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +264 -7
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -174
- package/src/infrastructure/searxng-lifecycle.ts +0 -577
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- 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 @
|
|
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 {
|
|
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
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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 @
|
|
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
|
|
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
|
-
*
|
|
44
|
+
* Thinking/Reasoning content block
|
|
35
45
|
*/
|
|
36
|
-
export
|
|
46
|
+
export interface ThinkingContentBlock {
|
|
47
|
+
type: 'thinking';
|
|
48
|
+
content: string;
|
|
49
|
+
}
|
|
37
50
|
|
|
38
51
|
/**
|
|
39
|
-
*
|
|
52
|
+
* Tool call block
|
|
40
53
|
*/
|
|
41
|
-
export
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
*
|
|
62
|
+
* Message content block
|
|
52
63
|
*/
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
+
|