pi-ui-extend 1.0.41 → 1.0.45
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/README.md +50 -13
- package/dist/app/app.d.ts +13 -0
- package/dist/app/app.js +158 -8
- package/dist/app/cli/install.js +2 -2
- package/dist/app/commands/command-controller.d.ts +4 -0
- package/dist/app/commands/command-controller.js +11 -0
- package/dist/app/commands/command-git-actions.d.ts +25 -0
- package/dist/app/commands/command-git-actions.js +381 -0
- package/dist/app/commands/command-host.d.ts +4 -0
- package/dist/app/commands/command-host.js +5 -2
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +57 -35
- package/dist/app/commands/command-navigation-actions.d.ts +1 -0
- package/dist/app/commands/command-navigation-actions.js +16 -3
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +15 -1
- package/dist/app/commands/command-session-actions.js +9 -3
- package/dist/app/commands/reload-context-inventory.d.ts +11 -0
- package/dist/app/commands/reload-context-inventory.js +70 -0
- package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
- package/dist/app/extensions/extension-actions-controller.js +6 -0
- package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
- package/dist/app/extensions/subagent-catalog-state.js +23 -0
- package/dist/app/input/autocomplete-controller.js +61 -34
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/input/input-controller.d.ts +2 -1
- package/dist/app/input/input-controller.js +7 -2
- package/dist/app/input/prompt-enhancer-controller.js +53 -35
- package/dist/app/input/voice-controller.d.ts +41 -45
- package/dist/app/input/voice-controller.js +351 -419
- package/dist/app/model/model-usage-controller.js +17 -7
- package/dist/app/model/model-usage-status.d.ts +4 -1
- package/dist/app/model/model-usage-status.js +270 -46
- package/dist/app/popup/menu-items-controller.d.ts +13 -3
- package/dist/app/popup/menu-items-controller.js +37 -21
- package/dist/app/popup/popup-action-controller.d.ts +12 -2
- package/dist/app/popup/popup-action-controller.js +77 -24
- package/dist/app/popup/popup-menu-controller.d.ts +36 -14
- package/dist/app/popup/popup-menu-controller.js +239 -69
- package/dist/app/rendering/dcp-stats.d.ts +9 -4
- package/dist/app/rendering/dcp-stats.js +40 -425
- package/dist/app/rendering/editor-panels.js +10 -4
- package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
- package/dist/app/rendering/popup-menu-renderer.js +42 -37
- package/dist/app/rendering/render-controller.js +23 -2
- package/dist/app/rendering/status-line-renderer.d.ts +5 -0
- package/dist/app/rendering/status-line-renderer.js +41 -5
- package/dist/app/rendering/tab-line-renderer.js +26 -20
- package/dist/app/runtime.d.ts +12 -1
- package/dist/app/runtime.js +120 -12
- package/dist/app/screen/mouse-controller.d.ts +2 -0
- package/dist/app/screen/mouse-controller.js +17 -7
- package/dist/app/screen/status-controller.d.ts +4 -0
- package/dist/app/screen/status-controller.js +5 -0
- package/dist/app/session/lazy-session-manager.js +34 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +10 -1
- package/dist/app/session/session-history.d.ts +1 -0
- package/dist/app/session/session-history.js +12 -1
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
- package/dist/app/session/session-lifecycle-controller.js +16 -1
- package/dist/app/session/tabs-controller.d.ts +26 -1
- package/dist/app/session/tabs-controller.js +454 -147
- package/dist/app/subagents/subagents-files.js +60 -1
- package/dist/app/subagents/subagents-model.d.ts +1 -0
- package/dist/app/subagents/subagents-model.js +18 -2
- package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
- package/dist/app/subagents/subagents-widget-controller.js +6 -0
- package/dist/app/types.d.ts +16 -1
- package/dist/app/workspace/workspace-actions-controller.js +10 -2
- package/dist/app/workspace/workspace-undo.d.ts +1 -0
- package/dist/app/workspace/workspace-undo.js +1 -0
- package/dist/bundled-extensions/question/index.js +9 -1
- package/dist/bundled-extensions/question/remote.d.ts +4 -0
- package/dist/bundled-extensions/question/remote.js +33 -0
- package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
- package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
- package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
- package/dist/bundled-extensions/telegram-connector/config.js +87 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
- package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
- package/dist/bundled-extensions/telegram-connector/index.js +195 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
- package/dist/bundled-extensions/terminal-bell/index.js +0 -76
- package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
- package/dist/bundled-extensions/workspace-undo/index.js +191 -0
- package/dist/config.d.ts +21 -2
- package/dist/config.js +159 -32
- package/dist/default-pix-config.js +25 -5
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
- package/dist/schemas/pi-tools-suite-schema.js +54 -83
- package/dist/schemas/pix-schema.d.ts +19 -2
- package/dist/schemas/pix-schema.js +47 -5
- package/dist/schemas/tasks-schema.d.ts +18 -0
- package/dist/schemas/tasks-schema.js +41 -0
- package/docs/concurrency.md +9 -1
- package/docs/desktop-mvp.md +22 -6
- package/docs/desktop-task-manager.md +148 -84
- package/docs/release.md +30 -6
- package/external/pi-tools-suite/README.md +197 -84
- package/external/pi-tools-suite/docs/evals.md +1 -1
- package/external/pi-tools-suite/docs/session-recovery.md +47 -14
- package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
- package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
- package/external/pi-tools-suite/package.json +6 -1
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
- package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
- package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
- package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
- package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
- package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
- package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
- package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
- package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
- package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
- package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
- package/external/pi-tools-suite/src/config.ts +55 -1
- package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
- package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
- package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
- package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
- package/external/pi-tools-suite/src/context-inventory.ts +99 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
- package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
- package/external/pi-tools-suite/src/dcp/config.ts +114 -17
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
- package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
- package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
- package/external/pi-tools-suite/src/dcp/index.ts +338 -66
- package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
- package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
- package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
- package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -0
- package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
- package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
- package/external/pi-tools-suite/src/index.ts +7 -1
- package/external/pi-tools-suite/src/lib/project.ts +36 -1
- package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
- package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
- package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
- package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
- package/external/pi-tools-suite/src/todo/index.ts +21 -0
- package/external/pi-tools-suite/src/todo/todo.ts +19 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
- package/package.json +10 -9
- package/schemas/pi-tools-suite.json +466 -287
- package/schemas/pix.json +129 -11
- package/schemas/tasks.json +131 -0
- package/skills/simplify/SKILL.md +33 -5
- package/docs/desktop-markdown-media.md +0 -77
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
- package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
- package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
- package/skills/playwright-cli/SKILL.md +0 -420
- package/skills/playwright-cli/references/element-attributes.md +0 -23
- package/skills/playwright-cli/references/playwright-tests.md +0 -50
- package/skills/playwright-cli/references/request-mocking.md +0 -87
- package/skills/playwright-cli/references/running-code.md +0 -241
- package/skills/playwright-cli/references/session-management.md +0 -273
- package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
- package/skills/playwright-cli/references/storage-state.md +0 -290
- package/skills/playwright-cli/references/test-generation.md +0 -142
- package/skills/playwright-cli/references/tracing.md +0 -154
- package/skills/playwright-cli/references/video-recording.md +0 -147
- package/skills/spec-lite/SKILL.md +0 -140
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
1
|
+
import { open, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { SUBAGENT_PARENT_SESSION_FILE, SUBAGENTS_REGISTRY_FILE, SUBAGENTS_RUN_ROOT } from "../constants.js";
|
|
4
4
|
import { isSubagentRegistry } from "./subagents-model.js";
|
|
5
|
+
const LAST_ACTIVITY_TAIL_BYTES = 64 * 1024;
|
|
5
6
|
export function subagentsRegistryPath(cwd) {
|
|
6
7
|
return join(cwd, SUBAGENTS_RUN_ROOT, SUBAGENTS_REGISTRY_FILE);
|
|
7
8
|
}
|
|
@@ -110,6 +111,9 @@ async function readSubagentAgentState(runDir, agentId, options) {
|
|
|
110
111
|
const finishedAt = await readTrimmedFile(join(agentDir, "finished_at"));
|
|
111
112
|
if (finishedAt)
|
|
112
113
|
state.finishedAt = finishedAt;
|
|
114
|
+
const lastActivity = await readLastActivity(join(agentDir, "progress.jsonl"));
|
|
115
|
+
if (lastActivity)
|
|
116
|
+
state.lastActivity = lastActivity;
|
|
113
117
|
const retryPending = await fileExists(join(agentDir, "retry_pending"));
|
|
114
118
|
const stopRequested = await fileExists(join(agentDir, "stop_requested"));
|
|
115
119
|
if (retryPending && !stopRequested && state.status !== "running" && state.status !== "done") {
|
|
@@ -179,6 +183,61 @@ async function readNumberFile(filePath) {
|
|
|
179
183
|
const value = Number.parseInt(raw, 10);
|
|
180
184
|
return Number.isFinite(value) ? value : undefined;
|
|
181
185
|
}
|
|
186
|
+
async function readLastActivity(filePath) {
|
|
187
|
+
let handle;
|
|
188
|
+
try {
|
|
189
|
+
handle = await open(filePath, "r");
|
|
190
|
+
const size = (await handle.stat()).size;
|
|
191
|
+
if (size <= 0)
|
|
192
|
+
return undefined;
|
|
193
|
+
const start = Math.max(0, size - LAST_ACTIVITY_TAIL_BYTES);
|
|
194
|
+
const buffer = Buffer.allocUnsafe(size - start);
|
|
195
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
|
|
196
|
+
if (bytesRead <= 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
const lines = buffer.toString("utf8", 0, bytesRead).split(/\r?\n/);
|
|
199
|
+
if (start > 0) {
|
|
200
|
+
const previousByte = Buffer.allocUnsafe(1);
|
|
201
|
+
const previousRead = await handle.read(previousByte, 0, 1, start - 1);
|
|
202
|
+
if (previousRead.bytesRead === 1 && previousByte[0] !== 0x0a)
|
|
203
|
+
lines.shift();
|
|
204
|
+
}
|
|
205
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
206
|
+
const line = lines[index]?.trim();
|
|
207
|
+
if (!line)
|
|
208
|
+
continue;
|
|
209
|
+
try {
|
|
210
|
+
const value = JSON.parse(line);
|
|
211
|
+
if (!isProgressActivityRecord(value))
|
|
212
|
+
continue;
|
|
213
|
+
const toolName = typeof value.toolName === "string" ? value.toolName.trim() : "";
|
|
214
|
+
if (toolName)
|
|
215
|
+
return { label: toolName, at: value.at };
|
|
216
|
+
if (value.type === "message_start" || value.type === "message_end")
|
|
217
|
+
return { label: "Thinking", at: value.at };
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Ignore malformed or partially written tail lines and keep scanning.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await handle?.close().catch(() => undefined);
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
function isProgressActivityRecord(value) {
|
|
233
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
234
|
+
return false;
|
|
235
|
+
const record = value;
|
|
236
|
+
return record.stage === "rpc_event"
|
|
237
|
+
&& typeof record.at === "string"
|
|
238
|
+
&& record.at.trim().length > 0
|
|
239
|
+
&& typeof record.type === "string";
|
|
240
|
+
}
|
|
182
241
|
async function countFileLines(filePath) {
|
|
183
242
|
try {
|
|
184
243
|
const content = await readFile(filePath, "utf8");
|
|
@@ -32,5 +32,6 @@ export declare function allSubagentStatesTerminal(agents: readonly SubagentAgent
|
|
|
32
32
|
export declare function taskPreviewMap(tasks: readonly SubagentTaskPreview[] | undefined): Map<string, SubagentTaskPreview>;
|
|
33
33
|
export declare function formatSubagentTimestamp(value: string | undefined): string | undefined;
|
|
34
34
|
export declare function formatElapsedSince(value: string | undefined, now?: number): string;
|
|
35
|
+
export declare function formatSubagentLastActivity(activity: SubagentAgentState["lastActivity"]): string | undefined;
|
|
35
36
|
export declare function subagentRunName(runDir: string): string;
|
|
36
37
|
export declare function resolveSubagentRunDir(cwd: string, runDir: string): string;
|
|
@@ -43,6 +43,14 @@ export function isSubagentAgentState(value) {
|
|
|
43
43
|
return false;
|
|
44
44
|
if (value.retryCount !== undefined && typeof value.retryCount !== "number")
|
|
45
45
|
return false;
|
|
46
|
+
if (value.lastActivity !== undefined) {
|
|
47
|
+
if (!isRecord(value.lastActivity))
|
|
48
|
+
return false;
|
|
49
|
+
if (typeof value.lastActivity.label !== "string" || !value.lastActivity.label.trim())
|
|
50
|
+
return false;
|
|
51
|
+
if (typeof value.lastActivity.at !== "string" || !value.lastActivity.at.trim())
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
46
54
|
return true;
|
|
47
55
|
}
|
|
48
56
|
export function isSubagentTaskPreview(value) {
|
|
@@ -219,11 +227,19 @@ export function formatSubagentTimestamp(value) {
|
|
|
219
227
|
return new Date(time).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
220
228
|
}
|
|
221
229
|
export function formatElapsedSince(value, now = Date.now()) {
|
|
230
|
+
return formatDurationSince(value, now) ?? "elapsed:—";
|
|
231
|
+
}
|
|
232
|
+
export function formatSubagentLastActivity(activity) {
|
|
233
|
+
if (!activity)
|
|
234
|
+
return undefined;
|
|
235
|
+
return activity.label.trim() || undefined;
|
|
236
|
+
}
|
|
237
|
+
function formatDurationSince(value, now) {
|
|
222
238
|
if (!value)
|
|
223
|
-
return
|
|
239
|
+
return undefined;
|
|
224
240
|
const started = Date.parse(value);
|
|
225
241
|
if (!Number.isFinite(started))
|
|
226
|
-
return
|
|
242
|
+
return undefined;
|
|
227
243
|
const seconds = Math.max(0, Math.floor((now - started) / 1000));
|
|
228
244
|
if (seconds < 60)
|
|
229
245
|
return `${seconds}s`;
|
|
@@ -22,6 +22,8 @@ export class AppSubagentsWidgetController {
|
|
|
22
22
|
this.host = host;
|
|
23
23
|
}
|
|
24
24
|
get widgetState() {
|
|
25
|
+
if (!this.host.hasActiveRuntime())
|
|
26
|
+
return undefined;
|
|
25
27
|
return this.state;
|
|
26
28
|
}
|
|
27
29
|
startPolling() {
|
|
@@ -51,6 +53,8 @@ export class AppSubagentsWidgetController {
|
|
|
51
53
|
return;
|
|
52
54
|
if (!isSubagentRunRenderDetails(details))
|
|
53
55
|
return;
|
|
56
|
+
if (!this.host.hasActiveRuntime())
|
|
57
|
+
return;
|
|
54
58
|
const runDir = resolveSubagentRunDir(this.host.cwd, details.runDir);
|
|
55
59
|
const normalizedDetails = { ...details, runDir };
|
|
56
60
|
if (normalizedDetails.tasks)
|
|
@@ -372,6 +376,8 @@ export class AppSubagentsWidgetController {
|
|
|
372
376
|
return activeSubagentStates(this.state?.agents ?? []).length > 0;
|
|
373
377
|
}
|
|
374
378
|
eventMatchesCurrentSession(eventSessionFile) {
|
|
379
|
+
if (!this.host.hasActiveRuntime())
|
|
380
|
+
return false;
|
|
375
381
|
if (!eventSessionFile)
|
|
376
382
|
return true;
|
|
377
383
|
const sessionFile = this.host.sessionFile();
|
package/dist/app/types.d.ts
CHANGED
|
@@ -139,6 +139,10 @@ export type SubagentStatus = (typeof SUBAGENT_STATUSES)[number];
|
|
|
139
139
|
export type SubagentActiveStatus = (typeof SUBAGENT_ACTIVE_STATUSES)[number];
|
|
140
140
|
export type SubagentTerminalStatus = (typeof SUBAGENT_TERMINAL_STATUSES)[number];
|
|
141
141
|
export type SubagentRenderMode = (typeof SUBAGENT_RENDER_MODES)[number];
|
|
142
|
+
export type SubagentActivity = {
|
|
143
|
+
label: string;
|
|
144
|
+
at: string;
|
|
145
|
+
};
|
|
142
146
|
export type SubagentAgentState = {
|
|
143
147
|
id: string;
|
|
144
148
|
status: SubagentStatus;
|
|
@@ -151,6 +155,7 @@ export type SubagentAgentState = {
|
|
|
151
155
|
stderrLines?: number;
|
|
152
156
|
eventLines?: number;
|
|
153
157
|
retryCount?: number;
|
|
158
|
+
lastActivity?: SubagentActivity;
|
|
154
159
|
};
|
|
155
160
|
export type SubagentTaskPreview = {
|
|
156
161
|
id: string;
|
|
@@ -230,6 +235,8 @@ export type RenderedLine = {
|
|
|
230
235
|
index: number;
|
|
231
236
|
} | {
|
|
232
237
|
kind: "popup-menu-close";
|
|
238
|
+
} | {
|
|
239
|
+
kind: "model-visibility-clear";
|
|
233
240
|
} | {
|
|
234
241
|
kind: "todo-panel";
|
|
235
242
|
} | {
|
|
@@ -341,6 +348,7 @@ export type SessionTab = {
|
|
|
341
348
|
id: string;
|
|
342
349
|
title: string;
|
|
343
350
|
titlePlaceholder?: "loading" | "new";
|
|
351
|
+
draft?: boolean;
|
|
344
352
|
isFork?: boolean;
|
|
345
353
|
status: SessionTabStatus;
|
|
346
354
|
activity?: SessionActivity;
|
|
@@ -537,6 +545,7 @@ export type ModelMenuValue = {
|
|
|
537
545
|
model: SessionModel;
|
|
538
546
|
ref: string;
|
|
539
547
|
current: boolean;
|
|
548
|
+
visible?: boolean;
|
|
540
549
|
};
|
|
541
550
|
export type ScopedSessionModel = {
|
|
542
551
|
model: SessionModel;
|
|
@@ -546,6 +555,12 @@ export type ThinkingMenuValue = {
|
|
|
546
555
|
level: ThinkingLevel;
|
|
547
556
|
current: boolean;
|
|
548
557
|
};
|
|
558
|
+
export type ModelThinkingMenuState = {
|
|
559
|
+
thinkingLevel: ThinkingLevel;
|
|
560
|
+
availableThinkingLevels: readonly ThinkingLevel[];
|
|
561
|
+
source: "model" | "thinking";
|
|
562
|
+
visibilityMode: boolean;
|
|
563
|
+
};
|
|
549
564
|
export type UserMessageMenuValue = "copy" | "fork" | "fork-new-tab" | "undo";
|
|
550
565
|
export type UserMessageJumpMenuValue = {
|
|
551
566
|
entryId?: string;
|
|
@@ -562,7 +577,7 @@ export type ResumeMenuValue = {
|
|
|
562
577
|
session: SessionInfo;
|
|
563
578
|
};
|
|
564
579
|
export type ActivePopupMenu = "slash" | "model" | "thinking" | "resume" | "user-message" | "user-message-jump" | "queue-message" | "sdk-menu";
|
|
565
|
-
export type PopupMenuPlacement = "default" | "under-tabs";
|
|
580
|
+
export type PopupMenuPlacement = "default" | "under-tabs" | "draft-surface";
|
|
566
581
|
export type StatusThinkingTarget = {
|
|
567
582
|
row: number;
|
|
568
583
|
startColumn: number;
|
|
@@ -2,7 +2,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { copyTextToClipboard } from "../screen/clipboard.js";
|
|
3
3
|
import { isRecord } from "../guards.js";
|
|
4
4
|
import { createId } from "../id.js";
|
|
5
|
-
import { loadWorkspaceUndoIndex, prepareWorkspaceMutation, revertWorkspaceMutations, saveWorkspaceUndoIndex, workspaceMutationFromToolExecution, workspaceUndoIndexKey, } from "./workspace-undo.js";
|
|
5
|
+
import { loadWorkspaceUndoIndex, prepareWorkspaceMutation, revertWorkspaceMutations, saveWorkspaceUndoIndex, workspaceMutationFromToolExecution, WORKSPACE_MUTATION_ENTRY_TYPE, workspaceUndoIndexKey, } from "./workspace-undo.js";
|
|
6
6
|
export class AppWorkspaceActionsController {
|
|
7
7
|
host;
|
|
8
8
|
workspaceUndoIndex = loadWorkspaceUndoIndex(getAgentDir());
|
|
@@ -23,8 +23,16 @@ export class AppWorkspaceActionsController {
|
|
|
23
23
|
return;
|
|
24
24
|
entry.workspaceMutations = [...(entry.workspaceMutations ?? []), mutation];
|
|
25
25
|
const sessionEntryId = this.resolveUserSessionEntryId(entry);
|
|
26
|
-
if (sessionEntryId)
|
|
26
|
+
if (sessionEntryId) {
|
|
27
27
|
this.persistWorkspaceMutations(sessionEntryId, entry.workspaceMutations);
|
|
28
|
+
// Also persist each new mutation in the session branch so Desktop and
|
|
29
|
+
// future renderers can perform the same message-scoped undo without
|
|
30
|
+
// relying on this process-wide compatibility index.
|
|
31
|
+
this.host.runtime()?.session.sessionManager.appendCustomEntry?.(WORKSPACE_MUTATION_ENTRY_TYPE, {
|
|
32
|
+
userEntryId: sessionEntryId,
|
|
33
|
+
mutation,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
28
36
|
this.host.touchEntry(entry);
|
|
29
37
|
}
|
|
30
38
|
scheduleUserSessionEntryMetadataSync() {
|
|
@@ -36,6 +36,7 @@ export type WorkspaceRevertResult = {
|
|
|
36
36
|
ok: false;
|
|
37
37
|
error: string;
|
|
38
38
|
};
|
|
39
|
+
export declare const WORKSPACE_MUTATION_ENTRY_TYPE = "pix-workspace-mutation";
|
|
39
40
|
export declare function workspaceUndoIndexKey(_sessionFile: string | undefined, _sessionId: string, entryId: string): string;
|
|
40
41
|
export declare function loadWorkspaceUndoIndex(agentDir: string): WorkspaceUndoIndex;
|
|
41
42
|
export declare function saveWorkspaceUndoIndex(agentDir: string, index: WorkspaceUndoIndex): void;
|
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { runProcess } from "../process.js";
|
|
5
5
|
const UNDO_INDEX_VERSION = 1;
|
|
6
|
+
export const WORKSPACE_MUTATION_ENTRY_TYPE = "pix-workspace-mutation";
|
|
6
7
|
export function workspaceUndoIndexKey(_sessionFile, _sessionId, entryId) {
|
|
7
8
|
return entryId;
|
|
8
9
|
}
|
|
@@ -3,6 +3,7 @@ import { questionParameters, normalizeQuestionInput } from "./contract.js";
|
|
|
3
3
|
import { renderQuestionCall, renderQuestionResult } from "./render.js";
|
|
4
4
|
import { createCanceledQuestionResult, createQuestionToolResult, createSuccessfulQuestionResult } from "./result.js";
|
|
5
5
|
import { runDesktopQuestionnaire, shouldUseDesktopQuestionBridge } from "./desktop.js";
|
|
6
|
+
import { runRemoteQuestionnaire } from "./remote.js";
|
|
6
7
|
import { runQuestionnaire } from "./tui.js";
|
|
7
8
|
export default function questionExtension(pi) {
|
|
8
9
|
pi.registerTool({
|
|
@@ -14,8 +15,15 @@ export default function questionExtension(pi) {
|
|
|
14
15
|
renderResult(result, _options, theme, context) {
|
|
15
16
|
return renderQuestionResult(result, theme, context.args);
|
|
16
17
|
},
|
|
17
|
-
async execute(_toolCallId, params,
|
|
18
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
18
19
|
const questions = normalizeQuestionInput(params);
|
|
20
|
+
const sessionId = ctx.sessionManager?.getSessionId();
|
|
21
|
+
const remoteSelections = sessionId ? await runRemoteQuestionnaire(sessionId, questions, signal) : undefined;
|
|
22
|
+
if (remoteSelections !== undefined) {
|
|
23
|
+
if (remoteSelections === null)
|
|
24
|
+
return createQuestionToolResult(createCanceledQuestionResult("user_canceled"), questions);
|
|
25
|
+
return createQuestionToolResult(createSuccessfulQuestionResult(questions, remoteSelections), questions);
|
|
26
|
+
}
|
|
19
27
|
if (!ctx.hasUI)
|
|
20
28
|
return createQuestionToolResult(createCanceledQuestionResult("ui_unavailable", questions), questions);
|
|
21
29
|
const selections = shouldUseDesktopQuestionBridge(ctx)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NormalizedQuestion, QuestionSelection } from "./types.js";
|
|
2
|
+
export type RemoteQuestionHandler = (questions: NormalizedQuestion[], signal?: AbortSignal) => Promise<QuestionSelection[] | null | undefined>;
|
|
3
|
+
export declare function registerRemoteQuestionHandler(sessionId: string, handler: RemoteQuestionHandler): () => void;
|
|
4
|
+
export declare function runRemoteQuestionnaire(sessionId: string, questions: NormalizedQuestion[], signal?: AbortSignal): Promise<QuestionSelection[] | null | undefined>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const REGISTRY_KEY = Symbol.for("pix.question.remote-registry.v1");
|
|
2
|
+
export function registerRemoteQuestionHandler(sessionId, handler) {
|
|
3
|
+
const registry = remoteQuestionRegistry();
|
|
4
|
+
registry.set(sessionId, handler);
|
|
5
|
+
let active = true;
|
|
6
|
+
return () => {
|
|
7
|
+
if (!active)
|
|
8
|
+
return;
|
|
9
|
+
active = false;
|
|
10
|
+
if (registry.get(sessionId) === handler)
|
|
11
|
+
registry.delete(sessionId);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export async function runRemoteQuestionnaire(sessionId, questions, signal) {
|
|
15
|
+
const handler = remoteQuestionRegistry().get(sessionId);
|
|
16
|
+
if (!handler)
|
|
17
|
+
return undefined;
|
|
18
|
+
try {
|
|
19
|
+
return await handler(questions, signal);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
return null;
|
|
24
|
+
// Remote integrations are optional. If Telegram/networking fails before an
|
|
25
|
+
// answer is obtained, fall back to the normal local question UI.
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function remoteQuestionRegistry() {
|
|
30
|
+
const root = globalThis;
|
|
31
|
+
root[REGISTRY_KEY] ??= new Map();
|
|
32
|
+
return root[REGISTRY_KEY];
|
|
33
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type TelegramBotClientOptions = {
|
|
2
|
+
sendTimeoutMs?: number;
|
|
3
|
+
pollRequestTimeoutMs?: number;
|
|
4
|
+
};
|
|
5
|
+
export type TelegramIncomingMessage = {
|
|
6
|
+
messageId: number;
|
|
7
|
+
chatId: string;
|
|
8
|
+
text: string;
|
|
9
|
+
replyToMessageId?: number;
|
|
10
|
+
};
|
|
11
|
+
type TelegramUpdate = {
|
|
12
|
+
update_id?: unknown;
|
|
13
|
+
message?: {
|
|
14
|
+
message_id?: unknown;
|
|
15
|
+
chat?: {
|
|
16
|
+
id?: unknown;
|
|
17
|
+
};
|
|
18
|
+
text?: unknown;
|
|
19
|
+
reply_to_message?: {
|
|
20
|
+
message_id?: unknown;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export type TelegramFetch = typeof fetch;
|
|
25
|
+
export declare class TelegramBotClient {
|
|
26
|
+
private readonly botToken;
|
|
27
|
+
private readonly chatId;
|
|
28
|
+
private readonly fetchFn;
|
|
29
|
+
private readonly options;
|
|
30
|
+
private running;
|
|
31
|
+
private abortController;
|
|
32
|
+
private nextOffset;
|
|
33
|
+
constructor(botToken: string, chatId: string, fetchFn?: TelegramFetch, options?: TelegramBotClientOptions);
|
|
34
|
+
start(onMessage: (message: TelegramIncomingMessage) => void | Promise<void>, onError?: (error: Error) => void): void;
|
|
35
|
+
stop(): void;
|
|
36
|
+
sendMessage(text: string, signal?: AbortSignal): Promise<number>;
|
|
37
|
+
private pollLoop;
|
|
38
|
+
private sanitizeError;
|
|
39
|
+
private call;
|
|
40
|
+
}
|
|
41
|
+
export declare function normalizeIncomingMessage(update: TelegramUpdate): TelegramIncomingMessage | undefined;
|
|
42
|
+
export declare function trimTelegramText(text: string): string;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
|
2
|
+
const MAX_TELEGRAM_TEXT = 4000;
|
|
3
|
+
const POLL_TIMEOUT_SECONDS = 25;
|
|
4
|
+
const DEFAULT_SEND_TIMEOUT_MS = 10_000;
|
|
5
|
+
const DEFAULT_POLL_REQUEST_TIMEOUT_MS = (POLL_TIMEOUT_SECONDS + 10) * 1000;
|
|
6
|
+
export class TelegramBotClient {
|
|
7
|
+
botToken;
|
|
8
|
+
chatId;
|
|
9
|
+
fetchFn;
|
|
10
|
+
options;
|
|
11
|
+
running = false;
|
|
12
|
+
abortController;
|
|
13
|
+
nextOffset;
|
|
14
|
+
constructor(botToken, chatId, fetchFn = fetch, options = {}) {
|
|
15
|
+
this.botToken = botToken;
|
|
16
|
+
this.chatId = chatId;
|
|
17
|
+
this.fetchFn = fetchFn;
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
start(onMessage, onError = () => { }) {
|
|
21
|
+
if (this.running)
|
|
22
|
+
return;
|
|
23
|
+
this.running = true;
|
|
24
|
+
this.abortController = new AbortController();
|
|
25
|
+
void this.pollLoop(onMessage, onError, this.abortController.signal);
|
|
26
|
+
}
|
|
27
|
+
stop() {
|
|
28
|
+
this.running = false;
|
|
29
|
+
this.abortController?.abort();
|
|
30
|
+
this.abortController = undefined;
|
|
31
|
+
}
|
|
32
|
+
async sendMessage(text, signal) {
|
|
33
|
+
try {
|
|
34
|
+
const body = await this.call("sendMessage", {
|
|
35
|
+
chat_id: this.chatId,
|
|
36
|
+
text: trimTelegramText(text),
|
|
37
|
+
disable_web_page_preview: true,
|
|
38
|
+
}, signal);
|
|
39
|
+
if (!body || typeof body.message_id !== "number")
|
|
40
|
+
throw new Error("Telegram sendMessage returned no message id");
|
|
41
|
+
return body.message_id;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw this.sanitizeError(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async pollLoop(onMessage, onError, signal) {
|
|
48
|
+
let failures = 0;
|
|
49
|
+
let baselineReady = false;
|
|
50
|
+
while (this.running && !signal.aborted) {
|
|
51
|
+
try {
|
|
52
|
+
if (!baselineReady) {
|
|
53
|
+
const pending = await this.call("getUpdates", {
|
|
54
|
+
offset: -1,
|
|
55
|
+
limit: 1,
|
|
56
|
+
timeout: 0,
|
|
57
|
+
allowed_updates: ["message"],
|
|
58
|
+
}, signal);
|
|
59
|
+
const latestUpdateId = pending
|
|
60
|
+
?.map((update) => update.update_id)
|
|
61
|
+
.filter((updateId) => typeof updateId === "number")
|
|
62
|
+
.at(-1);
|
|
63
|
+
if (latestUpdateId !== undefined)
|
|
64
|
+
this.nextOffset = latestUpdateId + 1;
|
|
65
|
+
baselineReady = true;
|
|
66
|
+
failures = 0;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const updates = await this.call("getUpdates", {
|
|
70
|
+
...(this.nextOffset === undefined ? {} : { offset: this.nextOffset }),
|
|
71
|
+
timeout: POLL_TIMEOUT_SECONDS,
|
|
72
|
+
allowed_updates: ["message"],
|
|
73
|
+
}, signal);
|
|
74
|
+
failures = 0;
|
|
75
|
+
for (const update of updates ?? []) {
|
|
76
|
+
if (typeof update.update_id === "number")
|
|
77
|
+
this.nextOffset = update.update_id + 1;
|
|
78
|
+
const message = normalizeIncomingMessage(update);
|
|
79
|
+
if (!message || message.chatId !== this.chatId)
|
|
80
|
+
continue;
|
|
81
|
+
await onMessage(message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (signal.aborted || !this.running)
|
|
86
|
+
return;
|
|
87
|
+
failures += 1;
|
|
88
|
+
onError(this.sanitizeError(error));
|
|
89
|
+
await sleep(Math.min(30_000, 500 * 2 ** Math.min(failures - 1, 6)), signal);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
sanitizeError(error) {
|
|
94
|
+
const message = asError(error).message.replaceAll(this.botToken, "[redacted]");
|
|
95
|
+
return new Error(message);
|
|
96
|
+
}
|
|
97
|
+
async call(method, payload, signal) {
|
|
98
|
+
const requestSignal = combineAbortSignals([
|
|
99
|
+
signal,
|
|
100
|
+
this.abortController?.signal,
|
|
101
|
+
], method === "getUpdates"
|
|
102
|
+
? this.options.pollRequestTimeoutMs ?? DEFAULT_POLL_REQUEST_TIMEOUT_MS
|
|
103
|
+
: this.options.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS);
|
|
104
|
+
const response = await this.fetchFn(`${TELEGRAM_API_BASE}/bot${this.botToken}/${method}`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: { "Content-Type": "application/json" },
|
|
107
|
+
body: JSON.stringify(payload),
|
|
108
|
+
signal: requestSignal,
|
|
109
|
+
});
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = await response.json();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Fall through to the sanitized HTTP error below.
|
|
116
|
+
}
|
|
117
|
+
if (!response.ok || parsed?.ok !== true) {
|
|
118
|
+
const description = typeof parsed?.description === "string" ? `: ${parsed.description}` : "";
|
|
119
|
+
throw new Error(`Telegram ${method} failed (${response.status})${description}`);
|
|
120
|
+
}
|
|
121
|
+
return parsed.result;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function combineAbortSignals(signals, timeoutMs) {
|
|
125
|
+
const active = signals.filter((signal) => signal !== undefined);
|
|
126
|
+
active.push(AbortSignal.timeout(Math.max(1, timeoutMs)));
|
|
127
|
+
return active.length === 1 ? active[0] : AbortSignal.any(active);
|
|
128
|
+
}
|
|
129
|
+
export function normalizeIncomingMessage(update) {
|
|
130
|
+
const message = update.message;
|
|
131
|
+
if (!message || typeof message.message_id !== "number" || typeof message.text !== "string")
|
|
132
|
+
return undefined;
|
|
133
|
+
const rawChatId = message.chat?.id;
|
|
134
|
+
if (typeof rawChatId !== "number" && typeof rawChatId !== "string")
|
|
135
|
+
return undefined;
|
|
136
|
+
const text = message.text.trim();
|
|
137
|
+
if (!text)
|
|
138
|
+
return undefined;
|
|
139
|
+
const rawReplyId = message.reply_to_message?.message_id;
|
|
140
|
+
return {
|
|
141
|
+
messageId: message.message_id,
|
|
142
|
+
chatId: String(rawChatId),
|
|
143
|
+
text,
|
|
144
|
+
...(typeof rawReplyId === "number" ? { replyToMessageId: rawReplyId } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function trimTelegramText(text) {
|
|
148
|
+
if (text.length <= MAX_TELEGRAM_TEXT)
|
|
149
|
+
return text;
|
|
150
|
+
return `${text.slice(0, MAX_TELEGRAM_TEXT - 2)}…`;
|
|
151
|
+
}
|
|
152
|
+
function asError(error) {
|
|
153
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
154
|
+
}
|
|
155
|
+
async function sleep(ms, signal) {
|
|
156
|
+
if (signal.aborted)
|
|
157
|
+
return;
|
|
158
|
+
await new Promise((resolve) => {
|
|
159
|
+
const timer = setTimeout(resolve, ms);
|
|
160
|
+
timer.unref?.();
|
|
161
|
+
signal.addEventListener("abort", () => {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
resolve();
|
|
164
|
+
}, { once: true });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type TelegramConnectorConfig = {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
botToken?: string;
|
|
4
|
+
chatId?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function getTelegramConnectorConfigPath(homeDir?: string): string;
|
|
7
|
+
export declare function readTelegramConnectorConfig(configPath?: string): TelegramConnectorConfig;
|
|
8
|
+
export declare function resolveTelegramConnectorConfig(configPath?: string, environment?: NodeJS.ProcessEnv): TelegramConnectorConfig;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
5
|
+
const CONFIG_KEY = "telegramConnector";
|
|
6
|
+
const ENV_ENABLED = "PIX_TELEGRAM_CONNECTOR";
|
|
7
|
+
const ENV_BOT_TOKEN = "PIX_TELEGRAM_BOT_TOKEN";
|
|
8
|
+
const ENV_CHAT_ID = "PIX_TELEGRAM_CHAT_ID";
|
|
9
|
+
export function getTelegramConnectorConfigPath(homeDir = homedir()) {
|
|
10
|
+
return join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
|
|
11
|
+
}
|
|
12
|
+
export function readTelegramConnectorConfig(configPath = getTelegramConnectorConfigPath()) {
|
|
13
|
+
return readTelegramConnectorConfigState(configPath).config;
|
|
14
|
+
}
|
|
15
|
+
function readTelegramConnectorConfigState(configPath) {
|
|
16
|
+
if (!existsSync(configPath))
|
|
17
|
+
return { config: { enabled: false }, explicitlyDisabled: false };
|
|
18
|
+
try {
|
|
19
|
+
const parsed = parseJsonc(readFileSync(configPath, "utf-8"));
|
|
20
|
+
if (!isRecord(parsed))
|
|
21
|
+
return { config: { enabled: false }, explicitlyDisabled: false };
|
|
22
|
+
const raw = parsed[CONFIG_KEY];
|
|
23
|
+
if (!isRecord(raw))
|
|
24
|
+
return { config: { enabled: false }, explicitlyDisabled: false };
|
|
25
|
+
const botToken = nonEmptyString(raw.botToken);
|
|
26
|
+
const chatId = normalizeChatId(raw.chatId);
|
|
27
|
+
const explicitlyDisabled = raw.enabled === false;
|
|
28
|
+
return {
|
|
29
|
+
config: {
|
|
30
|
+
enabled: !explicitlyDisabled && Boolean(botToken && chatId),
|
|
31
|
+
...(botToken ? { botToken } : {}),
|
|
32
|
+
...(chatId ? { chatId } : {}),
|
|
33
|
+
},
|
|
34
|
+
explicitlyDisabled,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { config: { enabled: false }, explicitlyDisabled: false };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function resolveTelegramConnectorConfig(configPath = getTelegramConnectorConfigPath(), environment = process.env) {
|
|
42
|
+
const { config: fromConfig, explicitlyDisabled } = readTelegramConnectorConfigState(configPath);
|
|
43
|
+
const envBotToken = nonEmptyString(environment[ENV_BOT_TOKEN]);
|
|
44
|
+
const envChatId = normalizeChatId(environment[ENV_CHAT_ID]);
|
|
45
|
+
const botToken = envBotToken ?? fromConfig.botToken;
|
|
46
|
+
const chatId = envChatId ?? fromConfig.chatId;
|
|
47
|
+
const envEnabled = parseBooleanEnv(environment[ENV_ENABLED]);
|
|
48
|
+
const enabled = envEnabled === false
|
|
49
|
+
? false
|
|
50
|
+
: envEnabled === true
|
|
51
|
+
? Boolean(botToken && chatId)
|
|
52
|
+
: explicitlyDisabled
|
|
53
|
+
? false
|
|
54
|
+
: Boolean(botToken && chatId);
|
|
55
|
+
return {
|
|
56
|
+
enabled,
|
|
57
|
+
...(botToken ? { botToken } : {}),
|
|
58
|
+
...(chatId ? { chatId } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function normalizeChatId(value) {
|
|
62
|
+
if (typeof value === "number" && Number.isSafeInteger(value))
|
|
63
|
+
return String(value);
|
|
64
|
+
if (typeof value !== "string")
|
|
65
|
+
return undefined;
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
return /^-?\d+$/.test(trimmed) ? trimmed : undefined;
|
|
68
|
+
}
|
|
69
|
+
function nonEmptyString(value) {
|
|
70
|
+
if (typeof value !== "string")
|
|
71
|
+
return undefined;
|
|
72
|
+
const trimmed = value.trim();
|
|
73
|
+
return trimmed || undefined;
|
|
74
|
+
}
|
|
75
|
+
function parseBooleanEnv(value) {
|
|
76
|
+
if (value === undefined || value.trim() === "")
|
|
77
|
+
return undefined;
|
|
78
|
+
const normalized = value.trim().toLowerCase();
|
|
79
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
80
|
+
return true;
|
|
81
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
82
|
+
return false;
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
function isRecord(value) {
|
|
86
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
87
|
+
}
|