pi-ui-extend 0.1.28 → 0.1.31
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/dist/app/app.d.ts +2 -0
- package/dist/app/app.js +31 -8
- package/dist/app/cli/update.d.ts +5 -0
- package/dist/app/cli/update.js +29 -1
- package/dist/app/model/model-usage-status.d.ts +2 -0
- package/dist/app/model/model-usage-status.js +90 -20
- package/dist/app/session/session-event-controller.d.ts +17 -1
- package/dist/app/session/session-event-controller.js +28 -0
- package/dist/app/session/tabs-controller.d.ts +10 -0
- package/dist/app/session/tabs-controller.js +65 -28
- package/external/pi-tools-suite/package.json +0 -3
- package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/index.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/render.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/tools/cleanup.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/tools/result.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +3 -3
- package/external/pi-tools-suite/src/async-subagents/tools/status.ts +3 -3
- package/external/pi-tools-suite/src/async-subagents/tools/stop.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -3
- package/external/pi-tools-suite/src/async-subagents/tools/wait.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/ui.ts +1 -1
- package/external/pi-tools-suite/src/dcp/commands.ts +2 -2
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +1 -1
- package/external/pi-tools-suite/src/dcp/index.ts +1 -1
- package/external/pi-tools-suite/src/lsp/constants.ts +1 -0
- package/external/pi-tools-suite/src/lsp/manager.ts +120 -71
- package/external/pi-tools-suite/src/model-tools/apply-patch.ts +1 -1
- package/external/pi-tools-suite/src/model-tools/index.ts +1 -1
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +2 -2
- package/external/pi-tools-suite/src/tool-descriptions.ts +2 -2
- package/external/pi-tools-suite/src/usage/lib/google.ts +39 -4
- package/package.json +4 -7
|
@@ -12,6 +12,7 @@ const BACKGROUND_PREWARM_TAB_LIMIT = 2;
|
|
|
12
12
|
const TAB_ATTENTION_BLINK_KEY = "tab-attention";
|
|
13
13
|
const LOADING_TAB_TITLE_PATTERN = /^loading(?:…|\.\.\.)?$/iu;
|
|
14
14
|
const DEFAULT_SESSION_TITLE_PATTERN = /^session [0-9a-f]{8}$/iu;
|
|
15
|
+
const SESSION_TITLE_HEAD_SCAN_MAX_BYTES = 256 * 1024;
|
|
15
16
|
const SESSION_TITLE_SCAN_MAX_BYTES = 2 * 1024 * 1024;
|
|
16
17
|
export class AppTabsController {
|
|
17
18
|
host;
|
|
@@ -23,6 +24,7 @@ export class AppTabsController {
|
|
|
23
24
|
historyReloadTimersByTabId = new Map();
|
|
24
25
|
inputStatesByTabId = new Map();
|
|
25
26
|
deferredUserMessagesByTabId = new Map();
|
|
27
|
+
sessionViewsByTabId = new Map();
|
|
26
28
|
tabIdsNeedingHistoryReload = new Set();
|
|
27
29
|
activeTabId;
|
|
28
30
|
pendingActiveTabId;
|
|
@@ -576,6 +578,7 @@ export class AppTabsController {
|
|
|
576
578
|
const previousRuntime = runtime;
|
|
577
579
|
const previousTargetActivity = target.activity;
|
|
578
580
|
this.storeActiveRuntime(runtime);
|
|
581
|
+
this.storeActiveSessionView();
|
|
579
582
|
this.storeActiveInputState();
|
|
580
583
|
this.storeActiveDeferredUserMessages();
|
|
581
584
|
this.activeTabId = target.id;
|
|
@@ -584,8 +587,6 @@ export class AppTabsController {
|
|
|
584
587
|
this.clearTabAttention(target);
|
|
585
588
|
this.restoreInputState(target.id);
|
|
586
589
|
this.host.closeMenusForTabSwitch?.();
|
|
587
|
-
this.host.resetSessionView();
|
|
588
|
-
this.restoreDeferredUserMessages(target.id);
|
|
589
590
|
this.host.setStatus("switching tab");
|
|
590
591
|
this.host.setSessionActivity("thinking");
|
|
591
592
|
this.host.render();
|
|
@@ -633,7 +634,18 @@ export class AppTabsController {
|
|
|
633
634
|
this.restoreInputState(target.id);
|
|
634
635
|
void this.saveTabs();
|
|
635
636
|
this.scheduleTabPrewarm();
|
|
636
|
-
|
|
637
|
+
const cachedView = this.sessionViewsByTabId.get(target.id);
|
|
638
|
+
if (cachedView && this.host.restoreSessionView) {
|
|
639
|
+
this.host.restoreSessionView(cachedView);
|
|
640
|
+
this.restoreDeferredUserMessages(target.id);
|
|
641
|
+
this.host.setSessionStatus(targetRuntime.session);
|
|
642
|
+
this.host.setSessionActivity(this.sessionActivity(targetRuntime.session));
|
|
643
|
+
this.host.render();
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
await this.loadActiveSessionHistory(targetRuntime);
|
|
647
|
+
this.tabIdsNeedingHistoryReload.delete(target.id);
|
|
648
|
+
}
|
|
637
649
|
this.scheduleDelayedHistoryReload(target.id, targetRuntime);
|
|
638
650
|
}
|
|
639
651
|
async closeTab(tabId) {
|
|
@@ -809,6 +821,11 @@ export class AppTabsController {
|
|
|
809
821
|
return;
|
|
810
822
|
this.setRuntimeForTab(this.activeTabId, runtime);
|
|
811
823
|
}
|
|
824
|
+
storeActiveSessionView() {
|
|
825
|
+
if (!this.activeTabId || !this.host.captureSessionView)
|
|
826
|
+
return;
|
|
827
|
+
this.sessionViewsByTabId.set(this.activeTabId, this.host.captureSessionView());
|
|
828
|
+
}
|
|
812
829
|
setRuntimeForTab(tabId, runtime) {
|
|
813
830
|
this.runtimesByTabId.set(tabId, runtime);
|
|
814
831
|
this.observeRuntimeForTab(tabId, runtime);
|
|
@@ -816,6 +833,7 @@ export class AppTabsController {
|
|
|
816
833
|
deleteRuntimeForTab(tabId) {
|
|
817
834
|
this.runtimesByTabId.delete(tabId);
|
|
818
835
|
this.runtimeLoadsByTabId.delete(tabId);
|
|
836
|
+
this.sessionViewsByTabId.delete(tabId);
|
|
819
837
|
this.clearRuntimeRefreshTimers(tabId);
|
|
820
838
|
this.clearHistoryReloadTimers(tabId);
|
|
821
839
|
this.tabIdsNeedingHistoryReload.delete(tabId);
|
|
@@ -900,11 +918,15 @@ export class AppTabsController {
|
|
|
900
918
|
return;
|
|
901
919
|
if (tabId !== this.activeTabId || this.pendingActiveTabId !== undefined)
|
|
902
920
|
return;
|
|
921
|
+
if (this.sessionActivity(runtime.session) === "running") {
|
|
922
|
+
this.clearHistoryReloadTimers(tabId);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
903
925
|
this.clearHistoryReloadTimers(tabId);
|
|
904
926
|
for (const delayMs of [150, 1000, 3000]) {
|
|
905
927
|
const timer = setTimeout(() => {
|
|
906
928
|
this.historyReloadTimersByTabId.get(tabId)?.delete(timer);
|
|
907
|
-
void this.reloadActiveTabHistoryIfNeeded(tabId, runtime
|
|
929
|
+
void this.reloadActiveTabHistoryIfNeeded(tabId, runtime);
|
|
908
930
|
}, delayMs);
|
|
909
931
|
timer.unref?.();
|
|
910
932
|
let timers = this.historyReloadTimersByTabId.get(tabId);
|
|
@@ -915,13 +937,15 @@ export class AppTabsController {
|
|
|
915
937
|
timers.add(timer);
|
|
916
938
|
}
|
|
917
939
|
}
|
|
918
|
-
async reloadActiveTabHistoryIfNeeded(tabId, runtime
|
|
940
|
+
async reloadActiveTabHistoryIfNeeded(tabId, runtime) {
|
|
919
941
|
if (tabId !== this.activeTabId || this.pendingActiveTabId !== undefined || this.host.runtime() !== runtime)
|
|
920
942
|
return;
|
|
921
943
|
if (!this.tabIdsNeedingHistoryReload.has(tabId))
|
|
922
944
|
return;
|
|
945
|
+
if (this.sessionActivity(runtime.session) === "running")
|
|
946
|
+
return;
|
|
923
947
|
await this.loadActiveSessionHistory(runtime);
|
|
924
|
-
if (
|
|
948
|
+
if (tabId === this.activeTabId && this.host.runtime() === runtime) {
|
|
925
949
|
this.tabIdsNeedingHistoryReload.delete(tabId);
|
|
926
950
|
}
|
|
927
951
|
}
|
|
@@ -1027,6 +1051,7 @@ export class AppTabsController {
|
|
|
1027
1051
|
this.clearRuntimeSubscriptions();
|
|
1028
1052
|
this.inputStatesByTabId.clear();
|
|
1029
1053
|
this.deferredUserMessagesByTabId.clear();
|
|
1054
|
+
this.sessionViewsByTabId.clear();
|
|
1030
1055
|
const seen = new Set();
|
|
1031
1056
|
for (const tab of tabs) {
|
|
1032
1057
|
const sessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
|
|
@@ -1505,28 +1530,14 @@ async function readLatestSessionTitle(sessionPath) {
|
|
|
1505
1530
|
const { size } = await file.stat();
|
|
1506
1531
|
if (size <= 0)
|
|
1507
1532
|
return undefined;
|
|
1508
|
-
const
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const line = lines[index]?.trim();
|
|
1517
|
-
if (!line)
|
|
1518
|
-
continue;
|
|
1519
|
-
let parsed;
|
|
1520
|
-
try {
|
|
1521
|
-
parsed = JSON.parse(line);
|
|
1522
|
-
}
|
|
1523
|
-
catch {
|
|
1524
|
-
continue;
|
|
1525
|
-
}
|
|
1526
|
-
if (!isRecord(parsed) || parsed.type !== "session_info" || typeof parsed.name !== "string")
|
|
1527
|
-
continue;
|
|
1528
|
-
return validSessionTitle(parsed.name);
|
|
1529
|
-
}
|
|
1533
|
+
const tailByteCount = Math.min(size, SESSION_TITLE_SCAN_MAX_BYTES);
|
|
1534
|
+
const tailTitle = await readSessionTitleChunk(file, size - tailByteCount, tailByteCount, { dropFirstLine: size > tailByteCount });
|
|
1535
|
+
if (tailTitle)
|
|
1536
|
+
return tailTitle;
|
|
1537
|
+
if (size <= tailByteCount)
|
|
1538
|
+
return undefined;
|
|
1539
|
+
const headByteCount = Math.min(size - tailByteCount, SESSION_TITLE_HEAD_SCAN_MAX_BYTES);
|
|
1540
|
+
return await readSessionTitleChunk(file, 0, headByteCount, { dropLastLine: headByteCount < size });
|
|
1530
1541
|
}
|
|
1531
1542
|
catch {
|
|
1532
1543
|
return undefined;
|
|
@@ -1534,6 +1545,32 @@ async function readLatestSessionTitle(sessionPath) {
|
|
|
1534
1545
|
finally {
|
|
1535
1546
|
await file?.close();
|
|
1536
1547
|
}
|
|
1548
|
+
}
|
|
1549
|
+
async function readSessionTitleChunk(file, position, byteCount, options = {}) {
|
|
1550
|
+
if (byteCount <= 0)
|
|
1551
|
+
return undefined;
|
|
1552
|
+
const buffer = Buffer.alloc(byteCount);
|
|
1553
|
+
await file.read(buffer, 0, byteCount, position);
|
|
1554
|
+
const lines = buffer.toString("utf8").split("\n");
|
|
1555
|
+
if (options.dropFirstLine)
|
|
1556
|
+
lines.shift();
|
|
1557
|
+
if (options.dropLastLine)
|
|
1558
|
+
lines.pop();
|
|
1559
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1560
|
+
const line = lines[index]?.trim();
|
|
1561
|
+
if (!line)
|
|
1562
|
+
continue;
|
|
1563
|
+
let parsed;
|
|
1564
|
+
try {
|
|
1565
|
+
parsed = JSON.parse(line);
|
|
1566
|
+
}
|
|
1567
|
+
catch {
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
if (!isRecord(parsed) || parsed.type !== "session_info" || typeof parsed.name !== "string")
|
|
1571
|
+
continue;
|
|
1572
|
+
return validSessionTitle(parsed.name);
|
|
1573
|
+
}
|
|
1537
1574
|
return undefined;
|
|
1538
1575
|
}
|
|
1539
1576
|
function validSessionTitle(value) {
|
|
@@ -41,9 +41,6 @@
|
|
|
41
41
|
"@earendil-works/pi-ai": "*",
|
|
42
42
|
"@earendil-works/pi-coding-agent": "*",
|
|
43
43
|
"@earendil-works/pi-tui": "*",
|
|
44
|
-
"@mariozechner/pi-ai": "*",
|
|
45
|
-
"@mariozechner/pi-coding-agent": "*",
|
|
46
|
-
"@mariozechner/pi-tui": "*",
|
|
47
44
|
"typebox": "*"
|
|
48
45
|
},
|
|
49
46
|
"devDependencies": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { truncateToWidth, visibleWidth } from "@
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { AgentState } from "./lib.js";
|
|
3
3
|
import { modelName, plural, statusGlyph, statusLabel } from "./format.js";
|
|
4
4
|
import type { AgentTaskPreview, SubagentRunRenderDetails } from "./types.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
5
5
|
import { readResult, resolveSubagentAgentRunDir, validateBasename } from "../lib.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { ExtensionAPI } from "@
|
|
4
|
-
import { Type } from "@
|
|
5
|
-
import { Text } from "@
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
5
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
7
7
|
import type { AgentCompletionHandler, AgentTask, ResolvedAgentTaskConfig, Semaphore, SpawnedAgent } from "../lib.js";
|
|
8
8
|
import {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
3
|
-
import { Text } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
5
5
|
import { getRunState, resolveSubagentRunDir, validateBasename } from "../lib.js";
|
|
6
6
|
import { INLINE_RENDERING } from "../constants.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
4
4
|
import { getRunState, resolveSubagentRunDir, stopAgents, validateBasename, validateStopSignal } from "../lib.js";
|
|
5
5
|
import { INLINE_RENDERING } from "../constants.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
3
|
-
import { Text } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { asyncSubagentToolDescriptions } from "../../tool-descriptions.js";
|
|
5
5
|
import { hasIndexedProjectRoot } from "../../lib/project.js";
|
|
6
6
|
import type { AgentCompletionHandler } from "../lib.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
4
4
|
import { resolveSubagentRunDir, validateBasename } from "../lib.js";
|
|
5
5
|
import { INLINE_RENDERING } from "../constants.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@
|
|
2
|
-
import type { AutocompleteItem } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"
|
|
2
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui"
|
|
3
3
|
import type { DcpState } from "./state.js"
|
|
4
4
|
import type { DcpConfig } from "./config.js"
|
|
5
5
|
import type { DcpNudgeType } from "./pruner-types.js"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
import { Type } from "typebox"
|
|
6
|
-
import type { ExtensionAPI } from "@
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
7
7
|
import type { DcpState } from "./state.js"
|
|
8
8
|
import type { DcpConfig } from "./config.js"
|
|
9
9
|
import { clearDcpNudgeAnchors } from "./pruner.js"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Dynamic Context Pruning (DCP) — module entry point for pi-tools-suite
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
6
6
|
import { loadConfig } from "./config.js"
|
|
7
7
|
import {
|
|
8
8
|
createState,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const DEFAULT_STARTUP_TIMEOUT_MS = 45_000;
|
|
2
2
|
export const DEFAULT_DIAGNOSTICS_WAIT_MS = 10_000;
|
|
3
3
|
export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
export const DEFAULT_IDLE_SHUTDOWN_MS = 30_000;
|
|
4
5
|
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
5
6
|
export const SHUTDOWN_WRITE_TIMEOUT_MS = 100;
|
|
6
7
|
export const SHUTDOWN_TERM_TIMEOUT_MS = 2_000;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { DEFAULT_DIAGNOSTICS_WAIT_MS, DEFAULT_MAX_FILE_SIZE_BYTES, LSP_MANAGER_GLOBAL_KEY } from "./constants";
|
|
3
|
+
import { DEFAULT_DIAGNOSTICS_WAIT_MS, DEFAULT_IDLE_SHUTDOWN_MS, DEFAULT_MAX_FILE_SIZE_BYTES, LSP_MANAGER_GLOBAL_KEY } from "./constants";
|
|
4
4
|
import { DiagnosticsStore } from "./diagnostics-store";
|
|
5
5
|
import { LspClient } from "./client";
|
|
6
6
|
import { loadLspConfig } from "./_shared/config";
|
|
@@ -43,12 +43,51 @@ export class LspManager {
|
|
|
43
43
|
private readonly diagnostics = new DiagnosticsStore();
|
|
44
44
|
private readonly clients = new Map<string, LspClient>();
|
|
45
45
|
private readonly backoff = new Map<string, { retryAt: number; attempts: number; reason: string }>();
|
|
46
|
+
private idleShutdownTimer: ReturnType<typeof setTimeout> | undefined;
|
|
47
|
+
private activeOperations = 0;
|
|
48
|
+
private handlingSignal = false;
|
|
46
49
|
private readonly handleProcessExit = () => {
|
|
47
50
|
this.shutdownAllSync();
|
|
48
51
|
};
|
|
52
|
+
private readonly handleProcessSignal = (signal: NodeJS.Signals) => {
|
|
53
|
+
this.shutdownAllSync();
|
|
54
|
+
|
|
55
|
+
// Restore the platform default for the terminating signal. LSP servers are
|
|
56
|
+
// spawned detached so they can otherwise outlive Pi when the process is
|
|
57
|
+
// killed by a terminal/editor without a session_shutdown event.
|
|
58
|
+
if (this.handlingSignal) return;
|
|
59
|
+
this.handlingSignal = true;
|
|
60
|
+
process.kill(process.pid, signal);
|
|
61
|
+
};
|
|
49
62
|
|
|
50
63
|
constructor() {
|
|
51
64
|
process.once("exit", this.handleProcessExit);
|
|
65
|
+
process.once("SIGINT", this.handleProcessSignal);
|
|
66
|
+
process.once("SIGTERM", this.handleProcessSignal);
|
|
67
|
+
process.once("SIGHUP", this.handleProcessSignal);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private clearIdleShutdownTimer(): void {
|
|
71
|
+
if (!this.idleShutdownTimer) return;
|
|
72
|
+
clearTimeout(this.idleShutdownTimer);
|
|
73
|
+
this.idleShutdownTimer = undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private beginOperation(): void {
|
|
77
|
+
this.activeOperations += 1;
|
|
78
|
+
this.clearIdleShutdownTimer();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private endOperation(): void {
|
|
82
|
+
this.activeOperations = Math.max(0, this.activeOperations - 1);
|
|
83
|
+
if (this.activeOperations > 0 || this.clients.size === 0) return;
|
|
84
|
+
|
|
85
|
+
this.clearIdleShutdownTimer();
|
|
86
|
+
this.idleShutdownTimer = setTimeout(() => {
|
|
87
|
+
if (this.activeOperations > 0 || this.clients.size === 0) return;
|
|
88
|
+
void this.shutdownAll();
|
|
89
|
+
}, DEFAULT_IDLE_SHUTDOWN_MS);
|
|
90
|
+
this.idleShutdownTimer.unref?.();
|
|
52
91
|
}
|
|
53
92
|
|
|
54
93
|
async matchingServers(ctx: ExtensionContext, file: string): Promise<{ matches: MatchedServer[]; warnings: string[]; workspace: string }> {
|
|
@@ -104,93 +143,98 @@ export class LspManager {
|
|
|
104
143
|
}
|
|
105
144
|
|
|
106
145
|
async updateDiagnosticsForFile(ctx: ExtensionContext, file: string): Promise<string> {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
for (const match of matches) {
|
|
112
|
-
try {
|
|
113
|
-
const maxFileSizeBytes = match.server.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
114
|
-
if (!(await fileSizeAllowed(file, maxFileSizeBytes))) {
|
|
115
|
-
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: skipped ${match.relFile}; file exceeds maxFileSizeBytes (${maxFileSizeBytes})`);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
146
|
+
this.beginOperation();
|
|
147
|
+
try {
|
|
148
|
+
const { matches, warnings, workspace } = await this.matchingServers(ctx, file);
|
|
149
|
+
if (matches.length === 0) return formatWarnings("LSP diagnostics", warnings);
|
|
118
150
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// old error from a previous document version. Empty diagnostics published
|
|
122
|
-
// by the server are stored, but this local clear is not.
|
|
123
|
-
this.diagnostics.clear(match.server.id, match.root, filePathToUri(file));
|
|
124
|
-
|
|
125
|
-
const text = await readTextFile(file);
|
|
126
|
-
const client = await this.getClient(match.server, match.root, file, workspace, ctx.signal);
|
|
127
|
-
const languageId = languageIdForFile(match.server, file);
|
|
128
|
-
const startedAt = Date.now();
|
|
129
|
-
const doc = await client.openOrChange(file, languageId, text, ctx.signal);
|
|
130
|
-
await client.didSave(file);
|
|
131
|
-
const diagnosticsWaitMs = match.server.diagnosticsWaitMs ?? DEFAULT_DIAGNOSTICS_WAIT_MS;
|
|
132
|
-
|
|
133
|
-
// typescript-language-server sometimes does not emit a fresh
|
|
134
|
-
// textDocument/publishDiagnostics notification after didChange/didSave,
|
|
135
|
-
// even though tsserver can answer diagnostics synchronously. Prefer the
|
|
136
|
-
// explicit tsserver request when the server exposes it, so post-edit
|
|
137
|
-
// diagnostics don't degrade into a misleading publishDiagnostics timeout.
|
|
138
|
-
let tsserverFallbackError: string | undefined;
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
for (const match of matches) {
|
|
139
153
|
try {
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
144
|
-
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
154
|
+
const maxFileSizeBytes = match.server.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
155
|
+
if (!(await fileSizeAllowed(file, maxFileSizeBytes))) {
|
|
156
|
+
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: skipped ${match.relFile}; file exceeds maxFileSizeBytes (${maxFileSizeBytes})`);
|
|
145
157
|
continue;
|
|
146
158
|
}
|
|
147
|
-
} catch (error) {
|
|
148
|
-
tsserverFallbackError = (error as Error).message;
|
|
149
|
-
}
|
|
150
159
|
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
// Clear stale diagnostics before refreshing this file. The synchronous
|
|
161
|
+
// wait below must observe a fresh publishDiagnostics notification, not an
|
|
162
|
+
// old error from a previous document version. Empty diagnostics published
|
|
163
|
+
// by the server are stored, but this local clear is not.
|
|
164
|
+
this.diagnostics.clear(match.server.id, match.root, filePathToUri(file));
|
|
165
|
+
|
|
166
|
+
const text = await readTextFile(file);
|
|
167
|
+
const client = await this.getClient(match.server, match.root, file, workspace, ctx.signal);
|
|
168
|
+
const languageId = languageIdForFile(match.server, file);
|
|
169
|
+
const startedAt = Date.now();
|
|
170
|
+
const doc = await client.openOrChange(file, languageId, text, ctx.signal);
|
|
171
|
+
await client.didSave(file);
|
|
172
|
+
const diagnosticsWaitMs = match.server.diagnosticsWaitMs ?? DEFAULT_DIAGNOSTICS_WAIT_MS;
|
|
173
|
+
|
|
174
|
+
// typescript-language-server sometimes does not emit a fresh
|
|
175
|
+
// textDocument/publishDiagnostics notification after didChange/didSave,
|
|
176
|
+
// even though tsserver can answer diagnostics synchronously. Prefer the
|
|
177
|
+
// explicit tsserver request when the server exposes it, so post-edit
|
|
178
|
+
// diagnostics don't degrade into a misleading publishDiagnostics timeout.
|
|
179
|
+
let tsserverFallbackError: string | undefined;
|
|
153
180
|
try {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text,
|
|
181
|
+
const tsserverDiagnostics = await client.tsserverDiagnostics(file, text, diagnosticsWaitMs, ctx.signal);
|
|
182
|
+
if (tsserverDiagnostics !== undefined) {
|
|
183
|
+
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, tsserverDiagnostics);
|
|
157
184
|
this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
158
185
|
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
159
186
|
continue;
|
|
160
187
|
}
|
|
161
188
|
} catch (error) {
|
|
162
|
-
|
|
189
|
+
tsserverFallbackError = (error as Error).message;
|
|
163
190
|
}
|
|
164
|
-
}
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
let pullDiagnosticsError: string | undefined;
|
|
193
|
+
if (match.server.pullDiagnostics !== false) {
|
|
194
|
+
try {
|
|
195
|
+
const pulledDiagnostics = await client.pullDiagnostics(file, diagnosticsWaitMs, ctx.signal);
|
|
196
|
+
if (pulledDiagnostics !== undefined) {
|
|
197
|
+
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, pulledDiagnostics);
|
|
198
|
+
this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
199
|
+
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
pullDiagnosticsError = (error as Error).message;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (match.server.waitForPublishDiagnostics === false || diagnosticsWaitMs <= 0) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
169
210
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
211
|
+
const entry = await this.diagnostics.waitForFile(
|
|
212
|
+
match.server.id,
|
|
213
|
+
match.root,
|
|
214
|
+
file,
|
|
215
|
+
startedAt,
|
|
216
|
+
doc.version,
|
|
217
|
+
diagnosticsWaitMs,
|
|
218
|
+
ctx.signal,
|
|
219
|
+
);
|
|
220
|
+
if (!isFreshDiagnosticsEntry(entry, startedAt, doc.version)) {
|
|
221
|
+
const fallbackSuffix = tsserverFallbackError ? `; tsserver fallback failed: ${tsserverFallbackError}` : "";
|
|
222
|
+
const pullSuffix = pullDiagnosticsError ? `; pull diagnostics failed: ${pullDiagnosticsError}` : "";
|
|
223
|
+
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: timed out after ${diagnosticsWaitMs}ms waiting for fresh diagnostics for ${match.relFile}${fallbackSuffix}${pullSuffix}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, entry.diagnostics);
|
|
227
|
+
if (diagnostics !== entry.diagnostics) this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
228
|
+
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
229
|
+
} catch (error) {
|
|
230
|
+
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: ${(error as Error).message}`);
|
|
184
231
|
}
|
|
185
|
-
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, entry.diagnostics);
|
|
186
|
-
if (diagnostics !== entry.diagnostics) this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
187
|
-
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
188
|
-
} catch (error) {
|
|
189
|
-
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: ${(error as Error).message}`);
|
|
190
232
|
}
|
|
191
|
-
}
|
|
192
233
|
|
|
193
|
-
|
|
234
|
+
return [formatWarnings("LSP diagnostics", warnings), joinSections("LSP diagnostics", lines)].filter(Boolean).join("\n\n");
|
|
235
|
+
} finally {
|
|
236
|
+
this.endOperation();
|
|
237
|
+
}
|
|
194
238
|
}
|
|
195
239
|
|
|
196
240
|
async ensureDocumentForTool(ctx: ExtensionContext, inputPath: string): Promise<{ file: string; match: MatchedServer; client: LspClient; workspace: string } | undefined> {
|
|
@@ -215,15 +259,20 @@ export class LspManager {
|
|
|
215
259
|
}
|
|
216
260
|
|
|
217
261
|
async shutdownAll(): Promise<void> {
|
|
262
|
+
this.clearIdleShutdownTimer();
|
|
218
263
|
const clients = [...this.clients.values()];
|
|
219
264
|
this.clients.clear();
|
|
220
265
|
await Promise.allSettled(clients.map((client) => client.shutdown()));
|
|
221
266
|
}
|
|
222
267
|
|
|
223
268
|
shutdownAllSync(): void {
|
|
269
|
+
this.clearIdleShutdownTimer();
|
|
224
270
|
const clients = [...this.clients.values()];
|
|
225
271
|
this.clients.clear();
|
|
226
272
|
process.off("exit", this.handleProcessExit);
|
|
273
|
+
process.off("SIGINT", this.handleProcessSignal);
|
|
274
|
+
process.off("SIGTERM", this.handleProcessSignal);
|
|
275
|
+
process.off("SIGHUP", this.handleProcessSignal);
|
|
227
276
|
for (const client of clients) client.shutdownSync();
|
|
228
277
|
}
|
|
229
278
|
}
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { withFileMutationQueue } from "@
|
|
5
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { isPathInside } from "./path-utils";
|
|
7
7
|
|
|
8
8
|
export interface ApplyPatchResult {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type ExtensionContext,
|
|
12
12
|
type ToolDefinition,
|
|
13
13
|
type ToolRenderResultOptions,
|
|
14
|
-
} from "@
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { realpath } from "node:fs/promises";
|
|
16
16
|
import { resolve } from "node:path";
|
|
17
17
|
import { Type, type TSchema } from "typebox";
|
|
@@ -145,7 +145,7 @@ function appendWorkflowReminder(text: string, op: Op, state: TaskState): string
|
|
|
145
145
|
const lines = [text];
|
|
146
146
|
if (op.kind === "create" || op.kind === "batch_create") {
|
|
147
147
|
lines.push(
|
|
148
|
-
"Reminder: if this is a multi-step task, include a final todo item for the user-facing final report before completion. Give that final-report todo an explicit description/acceptance criteria: summarize changed files and behavior, list verification commands/results, mention any remaining manual action, and never replace the user-facing report with a compression/housekeeping note.",
|
|
148
|
+
"Reminder: if this is a multi-step task, include a final todo item for the user-facing final report before completion. Give that final-report todo an explicit description/acceptance criteria: summarize changed files and behavior, list verification commands/results, mention any remaining manual action, and never replace the user-facing report with a compression/housekeeping note. Close that report todo immediately before sending the report.",
|
|
149
149
|
);
|
|
150
150
|
const createdIds = new Set(op.kind === "create" ? [op.taskId] : op.ids);
|
|
151
151
|
const hasOlderUnfinished = !op.replacedCount && state.tasks.some((task) => {
|
|
@@ -167,7 +167,7 @@ function appendWorkflowReminder(text: string, op: Op, state: TaskState): string
|
|
|
167
167
|
}
|
|
168
168
|
if (hasInProgress) {
|
|
169
169
|
lines.push(
|
|
170
|
-
"Reminder: before your final response, update any finished todo items to completed.
|
|
170
|
+
"Reminder: before your final response, update any finished todo items to completed. If one todo is the final user-facing report step, mark it completed immediately before sending the report.",
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
return lines.join("\n\n");
|