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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { NormalizedQuestion, QuestionSelection } from "../question/types.js";
|
|
2
|
+
import { TelegramBotClient, type TelegramIncomingMessage } from "./bot.js";
|
|
3
|
+
import type { TelegramConnectorConfig } from "./config.js";
|
|
4
|
+
export type TelegramCompletionKind = "complete" | "error";
|
|
5
|
+
export type TelegramSessionEndpoint = {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
getTitle(): string;
|
|
9
|
+
isIdle(): boolean;
|
|
10
|
+
sendUserMessage(text: string): void;
|
|
11
|
+
dispatchNewSession(requestId: string): void;
|
|
12
|
+
};
|
|
13
|
+
type BotLike = Pick<TelegramBotClient, "start" | "stop" | "sendMessage">;
|
|
14
|
+
type BotFactory = (botToken: string, chatId: string) => BotLike;
|
|
15
|
+
export declare class TelegramConnectorCoordinator {
|
|
16
|
+
private readonly createBot;
|
|
17
|
+
private readonly reportError;
|
|
18
|
+
private readonly sessions;
|
|
19
|
+
private readonly completionRoutes;
|
|
20
|
+
private readonly pendingQuestions;
|
|
21
|
+
private readonly newSessionRequests;
|
|
22
|
+
private readonly newSessionSources;
|
|
23
|
+
private bot;
|
|
24
|
+
private botKey;
|
|
25
|
+
private focusSessionId;
|
|
26
|
+
private stopTimer;
|
|
27
|
+
constructor(createBot?: BotFactory, reportError?: (message: string) => void);
|
|
28
|
+
attachSession(endpoint: TelegramSessionEndpoint, config: TelegramConnectorConfig): void;
|
|
29
|
+
detachSession(sessionId: string, endpoint?: TelegramSessionEndpoint): void;
|
|
30
|
+
notifyCompletion(sessionId: string, kind: TelegramCompletionKind, summary?: string): Promise<void>;
|
|
31
|
+
askQuestions(sessionId: string, questions: NormalizedQuestion[], signal?: AbortSignal): Promise<QuestionSelection[] | null | undefined>;
|
|
32
|
+
createNewSessionRequest(sourceSessionId: string, task: string): string | undefined;
|
|
33
|
+
consumeNewSessionRequest(requestId: string, sourceSessionId: string): string | undefined;
|
|
34
|
+
finishNewSessionRequest(sourceSessionId: string): void;
|
|
35
|
+
announceNewSession(sessionId: string): Promise<void>;
|
|
36
|
+
sendStatus(): Promise<void>;
|
|
37
|
+
sendHelp(): Promise<void>;
|
|
38
|
+
sendNotice(text: string): Promise<void>;
|
|
39
|
+
handleIncoming(message: TelegramIncomingMessage): Promise<void>;
|
|
40
|
+
stop(): void;
|
|
41
|
+
private ensureBot;
|
|
42
|
+
private waitForQuestionReply;
|
|
43
|
+
private resolvePendingQuestion;
|
|
44
|
+
private cancelPendingQuestion;
|
|
45
|
+
private rememberCompletionRoute;
|
|
46
|
+
private hasPendingQuestionFor;
|
|
47
|
+
private pruneExpiredNewSessionRequests;
|
|
48
|
+
private scheduleStop;
|
|
49
|
+
private clearStopTimer;
|
|
50
|
+
}
|
|
51
|
+
type ParsedTelegramCommand = {
|
|
52
|
+
kind: "new";
|
|
53
|
+
task: string;
|
|
54
|
+
} | {
|
|
55
|
+
kind: "help";
|
|
56
|
+
} | {
|
|
57
|
+
kind: "status";
|
|
58
|
+
} | {
|
|
59
|
+
kind: "message";
|
|
60
|
+
text: string;
|
|
61
|
+
};
|
|
62
|
+
export declare function parseTelegramCommand(text: string): ParsedTelegramCommand;
|
|
63
|
+
export declare function parseQuestionAnswer(question: NormalizedQuestion, answer: string): QuestionSelection | undefined;
|
|
64
|
+
export declare function getTelegramConnectorCoordinator(): TelegramConnectorCoordinator;
|
|
65
|
+
export declare function resetTelegramConnectorCoordinatorForTests(): void;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { TelegramBotClient } from "./bot.js";
|
|
4
|
+
const COMPLETION_ROUTE_LIMIT = 200;
|
|
5
|
+
const NEW_SESSION_REQUEST_TTL_MS = 10 * 60_000;
|
|
6
|
+
const STOP_GRACE_MS = 750;
|
|
7
|
+
export class TelegramConnectorCoordinator {
|
|
8
|
+
createBot;
|
|
9
|
+
reportError;
|
|
10
|
+
sessions = new Map();
|
|
11
|
+
completionRoutes = new Map();
|
|
12
|
+
pendingQuestions = new Map();
|
|
13
|
+
newSessionRequests = new Map();
|
|
14
|
+
newSessionSources = new Map();
|
|
15
|
+
bot;
|
|
16
|
+
botKey;
|
|
17
|
+
focusSessionId;
|
|
18
|
+
stopTimer;
|
|
19
|
+
constructor(createBot = (botToken, chatId) => new TelegramBotClient(botToken, chatId), reportError = (message) => process.stderr.write(`[telegram-connector] ${message}\n`)) {
|
|
20
|
+
this.createBot = createBot;
|
|
21
|
+
this.reportError = reportError;
|
|
22
|
+
}
|
|
23
|
+
attachSession(endpoint, config) {
|
|
24
|
+
this.sessions.set(endpoint.sessionId, endpoint);
|
|
25
|
+
this.clearStopTimer();
|
|
26
|
+
if (!config.enabled || !config.botToken || !config.chatId)
|
|
27
|
+
return;
|
|
28
|
+
this.ensureBot(config.botToken, config.chatId);
|
|
29
|
+
}
|
|
30
|
+
detachSession(sessionId, endpoint) {
|
|
31
|
+
if (endpoint && this.sessions.get(sessionId) !== endpoint)
|
|
32
|
+
return;
|
|
33
|
+
this.sessions.delete(sessionId);
|
|
34
|
+
if (this.focusSessionId === sessionId)
|
|
35
|
+
this.focusSessionId = undefined;
|
|
36
|
+
for (const [messageId, pending] of this.pendingQuestions) {
|
|
37
|
+
if (pending.sessionId !== sessionId)
|
|
38
|
+
continue;
|
|
39
|
+
this.pendingQuestions.delete(messageId);
|
|
40
|
+
pending.resolve(null);
|
|
41
|
+
}
|
|
42
|
+
if (this.sessions.size === 0)
|
|
43
|
+
this.scheduleStop();
|
|
44
|
+
}
|
|
45
|
+
async notifyCompletion(sessionId, kind, summary) {
|
|
46
|
+
const endpoint = this.sessions.get(sessionId);
|
|
47
|
+
const bot = this.bot;
|
|
48
|
+
if (!endpoint || !bot)
|
|
49
|
+
return;
|
|
50
|
+
const messageId = await bot.sendMessage(formatCompletionMessage(endpoint, kind, summary));
|
|
51
|
+
if (this.bot !== bot || this.sessions.get(sessionId) !== endpoint)
|
|
52
|
+
return;
|
|
53
|
+
this.focusSessionId = sessionId;
|
|
54
|
+
this.rememberCompletionRoute(messageId, sessionId);
|
|
55
|
+
}
|
|
56
|
+
async askQuestions(sessionId, questions, signal) {
|
|
57
|
+
const endpoint = this.sessions.get(sessionId);
|
|
58
|
+
const bot = this.bot;
|
|
59
|
+
if (!endpoint || !bot)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (signal?.aborted)
|
|
62
|
+
return null;
|
|
63
|
+
this.focusSessionId = sessionId;
|
|
64
|
+
const selections = [];
|
|
65
|
+
for (let index = 0; index < questions.length; index++) {
|
|
66
|
+
const question = questions[index];
|
|
67
|
+
const messageId = await bot.sendMessage(formatQuestionMessage(endpoint, question, index, questions.length), signal);
|
|
68
|
+
if (signal?.aborted || this.bot !== bot || this.sessions.get(sessionId) !== endpoint)
|
|
69
|
+
return null;
|
|
70
|
+
let answer = await this.waitForQuestionReply(messageId, sessionId, signal);
|
|
71
|
+
while (true) {
|
|
72
|
+
if (answer === null || isCancelCommand(answer))
|
|
73
|
+
return null;
|
|
74
|
+
const parsed = parseQuestionAnswer(question, answer);
|
|
75
|
+
if (parsed) {
|
|
76
|
+
selections.push(parsed);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
// Re-arm the route before awaiting Telegram feedback. Otherwise a quick
|
|
80
|
+
// corrected reply can arrive after the invalid reply removed the old
|
|
81
|
+
// pending entry but before the feedback send completes.
|
|
82
|
+
const nextAnswer = this.waitForQuestionReply(messageId, sessionId, signal);
|
|
83
|
+
try {
|
|
84
|
+
await bot.sendMessage("Не понял выбор. Ответьте на исходный вопрос номером варианта, несколькими номерами через запятую или своим текстом.", signal);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.cancelPendingQuestion(messageId, sessionId);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if (signal?.aborted || this.bot !== bot || this.sessions.get(sessionId) !== endpoint) {
|
|
91
|
+
this.cancelPendingQuestion(messageId, sessionId);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
answer = await nextAnswer;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return selections;
|
|
98
|
+
}
|
|
99
|
+
createNewSessionRequest(sourceSessionId, task) {
|
|
100
|
+
const endpoint = this.sessions.get(sourceSessionId);
|
|
101
|
+
const normalizedTask = task.trim();
|
|
102
|
+
if (!endpoint || !normalizedTask)
|
|
103
|
+
return undefined;
|
|
104
|
+
this.pruneExpiredNewSessionRequests();
|
|
105
|
+
if ((this.newSessionSources.get(sourceSessionId) ?? 0) > Date.now())
|
|
106
|
+
return undefined;
|
|
107
|
+
const requestId = randomUUID();
|
|
108
|
+
const expiresAt = Date.now() + NEW_SESSION_REQUEST_TTL_MS;
|
|
109
|
+
this.newSessionRequests.set(requestId, {
|
|
110
|
+
sourceSessionId,
|
|
111
|
+
task: normalizedTask,
|
|
112
|
+
expiresAt,
|
|
113
|
+
});
|
|
114
|
+
this.newSessionSources.set(sourceSessionId, expiresAt);
|
|
115
|
+
try {
|
|
116
|
+
endpoint.dispatchNewSession(requestId);
|
|
117
|
+
this.focusSessionId = sourceSessionId;
|
|
118
|
+
return requestId;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
this.newSessionRequests.delete(requestId);
|
|
122
|
+
this.newSessionSources.delete(sourceSessionId);
|
|
123
|
+
this.reportError(errorMessage(error));
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
consumeNewSessionRequest(requestId, sourceSessionId) {
|
|
128
|
+
this.pruneExpiredNewSessionRequests();
|
|
129
|
+
const request = this.newSessionRequests.get(requestId);
|
|
130
|
+
if (!request || request.sourceSessionId !== sourceSessionId)
|
|
131
|
+
return undefined;
|
|
132
|
+
this.newSessionRequests.delete(requestId);
|
|
133
|
+
return request.task;
|
|
134
|
+
}
|
|
135
|
+
finishNewSessionRequest(sourceSessionId) {
|
|
136
|
+
this.newSessionSources.delete(sourceSessionId);
|
|
137
|
+
}
|
|
138
|
+
async announceNewSession(sessionId) {
|
|
139
|
+
const endpoint = this.sessions.get(sessionId);
|
|
140
|
+
const bot = this.bot;
|
|
141
|
+
if (!endpoint || !bot)
|
|
142
|
+
return;
|
|
143
|
+
const messageId = await bot.sendMessage(`🆕 Новая сессия · ${sessionLabel(endpoint)}\nСессия создана. Задача запускается.`);
|
|
144
|
+
if (this.bot !== bot || this.sessions.get(sessionId) !== endpoint)
|
|
145
|
+
return;
|
|
146
|
+
this.focusSessionId = sessionId;
|
|
147
|
+
this.rememberCompletionRoute(messageId, sessionId);
|
|
148
|
+
}
|
|
149
|
+
async sendStatus() {
|
|
150
|
+
if (!this.bot)
|
|
151
|
+
return;
|
|
152
|
+
const endpoint = this.focusSessionId ? this.sessions.get(this.focusSessionId) : undefined;
|
|
153
|
+
if (!endpoint) {
|
|
154
|
+
await this.bot.sendMessage("Нет активной Telegram-сессии. Дождитесь уведомления от Pix или запустите задачу локально.");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await this.bot.sendMessage(`${endpoint.isIdle() ? "🟢 idle" : "🟡 busy"} · ${sessionLabel(endpoint)}`);
|
|
158
|
+
}
|
|
159
|
+
async sendHelp() {
|
|
160
|
+
if (!this.bot)
|
|
161
|
+
return;
|
|
162
|
+
await this.bot.sendMessage([
|
|
163
|
+
"Telegram connector",
|
|
164
|
+
"",
|
|
165
|
+
"↩️ Ответьте на уведомление — продолжить в той же сессии.",
|
|
166
|
+
"🆕 /new <задача> — новая сессия (можно ответом на конкретное уведомление).",
|
|
167
|
+
"📍 /status — текущая сессия.",
|
|
168
|
+
"❌ /cancel — отменить ожидающий Telegram-вопрос.",
|
|
169
|
+
].join("\n"));
|
|
170
|
+
}
|
|
171
|
+
async sendNotice(text) {
|
|
172
|
+
await this.bot?.sendMessage(text);
|
|
173
|
+
}
|
|
174
|
+
async handleIncoming(message) {
|
|
175
|
+
const explicitQuestion = message.replyToMessageId === undefined ? undefined : this.pendingQuestions.get(message.replyToMessageId);
|
|
176
|
+
if (explicitQuestion) {
|
|
177
|
+
this.resolvePendingQuestion(message.replyToMessageId, explicitQuestion, message.text);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const command = parseTelegramCommand(message.text);
|
|
181
|
+
if (message.replyToMessageId === undefined
|
|
182
|
+
&& this.pendingQuestions.size === 1
|
|
183
|
+
&& (command.kind === "message" || isCancelCommand(message.text))) {
|
|
184
|
+
const [messageId, pending] = this.pendingQuestions.entries().next().value;
|
|
185
|
+
this.resolvePendingQuestion(messageId, pending, message.text);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (message.replyToMessageId === undefined
|
|
189
|
+
&& this.pendingQuestions.size > 1
|
|
190
|
+
&& (command.kind === "message" || isCancelCommand(message.text))) {
|
|
191
|
+
await this.bot?.sendMessage("Есть несколько ожидающих вопросов. Ответьте reply на конкретное сообщение с вопросом.");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (command.kind === "help") {
|
|
195
|
+
await this.sendHelp();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (command.kind === "status") {
|
|
199
|
+
await this.sendStatus();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (message.replyToMessageId === undefined && this.sessions.size > 1) {
|
|
203
|
+
await this.bot?.sendMessage("Есть несколько живых сессий. Ответьте reply на уведомление нужной сессии, чтобы не отправить задачу не туда.");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const routedSessionId = message.replyToMessageId === undefined
|
|
207
|
+
? this.focusSessionId
|
|
208
|
+
: this.completionRoutes.get(message.replyToMessageId);
|
|
209
|
+
const endpoint = routedSessionId ? this.sessions.get(routedSessionId) : undefined;
|
|
210
|
+
if (!endpoint) {
|
|
211
|
+
await this.bot?.sendMessage("Не могу определить живую сессию для этого сообщения. Ответьте на свежее уведомление от Pix.");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.focusSessionId = endpoint.sessionId;
|
|
215
|
+
if (command.kind === "new") {
|
|
216
|
+
if (!command.task) {
|
|
217
|
+
await this.bot?.sendMessage("Использование: /new <задача>");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (this.hasPendingQuestionFor(endpoint.sessionId)) {
|
|
221
|
+
await this.bot?.sendMessage("Эта сессия ждёт ответа на вопрос. Сначала ответьте на вопрос или отправьте /cancel reply на него.");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const requestId = this.createNewSessionRequest(endpoint.sessionId, command.task);
|
|
225
|
+
await this.bot?.sendMessage(requestId ? "🆕 Создаю новую сессию и передаю задачу." : "Не удалось запустить новую сессию.");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
endpoint.sendUserMessage(command.text);
|
|
230
|
+
await this.bot?.sendMessage(`➡️ Отправлено · ${sessionLabel(endpoint)}`);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
this.reportError(errorMessage(error));
|
|
234
|
+
await this.bot?.sendMessage("Не удалось отправить сообщение: сессия уже заменена или недоступна.");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
stop() {
|
|
238
|
+
this.clearStopTimer();
|
|
239
|
+
this.bot?.stop();
|
|
240
|
+
this.bot = undefined;
|
|
241
|
+
this.botKey = undefined;
|
|
242
|
+
for (const pending of this.pendingQuestions.values())
|
|
243
|
+
pending.resolve(null);
|
|
244
|
+
this.pendingQuestions.clear();
|
|
245
|
+
this.completionRoutes.clear();
|
|
246
|
+
this.newSessionRequests.clear();
|
|
247
|
+
this.newSessionSources.clear();
|
|
248
|
+
this.focusSessionId = undefined;
|
|
249
|
+
}
|
|
250
|
+
ensureBot(botToken, chatId) {
|
|
251
|
+
const key = `${botToken}\u0000${chatId}`;
|
|
252
|
+
if (this.bot && this.botKey === key)
|
|
253
|
+
return;
|
|
254
|
+
this.stop();
|
|
255
|
+
this.botKey = key;
|
|
256
|
+
this.bot = this.createBot(botToken, chatId);
|
|
257
|
+
this.bot.start((message) => this.handleIncoming(message), (error) => this.reportError(error.message));
|
|
258
|
+
}
|
|
259
|
+
waitForQuestionReply(messageId, sessionId, signal) {
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
let settled = false;
|
|
262
|
+
const onAbort = () => settle(null);
|
|
263
|
+
const settle = (text) => {
|
|
264
|
+
if (settled)
|
|
265
|
+
return;
|
|
266
|
+
settled = true;
|
|
267
|
+
signal?.removeEventListener("abort", onAbort);
|
|
268
|
+
if (this.pendingQuestions.get(messageId)?.resolve === settle)
|
|
269
|
+
this.pendingQuestions.delete(messageId);
|
|
270
|
+
resolve(text);
|
|
271
|
+
};
|
|
272
|
+
this.pendingQuestions.set(messageId, { sessionId, resolve: settle });
|
|
273
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
274
|
+
if (signal?.aborted)
|
|
275
|
+
onAbort();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
resolvePendingQuestion(messageId, pending, text) {
|
|
279
|
+
this.pendingQuestions.delete(messageId);
|
|
280
|
+
this.focusSessionId = pending.sessionId;
|
|
281
|
+
pending.resolve(text);
|
|
282
|
+
}
|
|
283
|
+
cancelPendingQuestion(messageId, sessionId) {
|
|
284
|
+
const pending = this.pendingQuestions.get(messageId);
|
|
285
|
+
if (!pending || pending.sessionId !== sessionId)
|
|
286
|
+
return;
|
|
287
|
+
this.pendingQuestions.delete(messageId);
|
|
288
|
+
pending.resolve(null);
|
|
289
|
+
}
|
|
290
|
+
rememberCompletionRoute(messageId, sessionId) {
|
|
291
|
+
this.completionRoutes.set(messageId, sessionId);
|
|
292
|
+
while (this.completionRoutes.size > COMPLETION_ROUTE_LIMIT) {
|
|
293
|
+
const oldest = this.completionRoutes.keys().next().value;
|
|
294
|
+
if (oldest === undefined)
|
|
295
|
+
break;
|
|
296
|
+
this.completionRoutes.delete(oldest);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
hasPendingQuestionFor(sessionId) {
|
|
300
|
+
return [...this.pendingQuestions.values()].some((pending) => pending.sessionId === sessionId);
|
|
301
|
+
}
|
|
302
|
+
pruneExpiredNewSessionRequests() {
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
for (const [requestId, request] of this.newSessionRequests) {
|
|
305
|
+
if (request.expiresAt <= now)
|
|
306
|
+
this.newSessionRequests.delete(requestId);
|
|
307
|
+
}
|
|
308
|
+
for (const [sessionId, expiresAt] of this.newSessionSources) {
|
|
309
|
+
if (expiresAt <= now)
|
|
310
|
+
this.newSessionSources.delete(sessionId);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
scheduleStop() {
|
|
314
|
+
this.clearStopTimer();
|
|
315
|
+
this.stopTimer = setTimeout(() => {
|
|
316
|
+
this.stopTimer = undefined;
|
|
317
|
+
if (this.sessions.size === 0)
|
|
318
|
+
this.stop();
|
|
319
|
+
}, STOP_GRACE_MS);
|
|
320
|
+
this.stopTimer.unref?.();
|
|
321
|
+
}
|
|
322
|
+
clearStopTimer() {
|
|
323
|
+
if (!this.stopTimer)
|
|
324
|
+
return;
|
|
325
|
+
clearTimeout(this.stopTimer);
|
|
326
|
+
this.stopTimer = undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export function parseTelegramCommand(text) {
|
|
330
|
+
const trimmed = text.trim();
|
|
331
|
+
const commandMatch = /^\/(new|help|status)(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/i.exec(trimmed);
|
|
332
|
+
if (!commandMatch)
|
|
333
|
+
return { kind: "message", text: trimmed };
|
|
334
|
+
const command = commandMatch[1].toLowerCase();
|
|
335
|
+
if (command === "help")
|
|
336
|
+
return { kind: "help" };
|
|
337
|
+
if (command === "status")
|
|
338
|
+
return { kind: "status" };
|
|
339
|
+
return { kind: "new", task: commandMatch[2]?.trim() ?? "" };
|
|
340
|
+
}
|
|
341
|
+
export function parseQuestionAnswer(question, answer) {
|
|
342
|
+
const trimmed = answer.trim();
|
|
343
|
+
if (!trimmed || isCancelCommand(trimmed))
|
|
344
|
+
return undefined;
|
|
345
|
+
const numeric = trimmed.match(/^\d+(?:\s*[, ]\s*\d+)*$/)?.[0];
|
|
346
|
+
if (!numeric) {
|
|
347
|
+
if (!question.multiple)
|
|
348
|
+
return { id: question.id, customText: trimmed };
|
|
349
|
+
if ((question.minSelections ?? 1) > 1 || (question.maxSelections ?? question.choices.length + 1) < 1)
|
|
350
|
+
return undefined;
|
|
351
|
+
return { id: question.id, choiceValues: [], customText: trimmed };
|
|
352
|
+
}
|
|
353
|
+
const indexes = numeric.split(/\s*[, ]\s*/).filter(Boolean).map((value) => Number(value));
|
|
354
|
+
if (indexes.some((index) => !Number.isInteger(index) || index < 1 || index > question.choices.length))
|
|
355
|
+
return undefined;
|
|
356
|
+
const unique = [...new Set(indexes)];
|
|
357
|
+
if (question.multiple) {
|
|
358
|
+
if (unique.length < (question.minSelections ?? 1) || unique.length > (question.maxSelections ?? question.choices.length + 1))
|
|
359
|
+
return undefined;
|
|
360
|
+
return { id: question.id, choiceValues: unique.map((index) => question.choices[index - 1].value) };
|
|
361
|
+
}
|
|
362
|
+
if (unique.length !== 1)
|
|
363
|
+
return undefined;
|
|
364
|
+
return { id: question.id, choiceValue: question.choices[unique[0] - 1].value };
|
|
365
|
+
}
|
|
366
|
+
function formatCompletionMessage(endpoint, kind, summary) {
|
|
367
|
+
const heading = kind === "error" ? "🔴 Ошибка" : "✅ Готово";
|
|
368
|
+
const body = summary?.trim();
|
|
369
|
+
return [
|
|
370
|
+
`${heading} · ${sessionLabel(endpoint)}`,
|
|
371
|
+
...(body ? ["", body.slice(0, 2500)] : []),
|
|
372
|
+
"",
|
|
373
|
+
"↩️ Ответьте на это сообщение — продолжить в этой сессии.",
|
|
374
|
+
"🆕 /new <задача> — начать новую сессию.",
|
|
375
|
+
].join("\n");
|
|
376
|
+
}
|
|
377
|
+
function formatQuestionMessage(endpoint, question, index, total) {
|
|
378
|
+
const choices = question.choices.map((choice, choiceIndex) => {
|
|
379
|
+
const description = choice.description ? ` — ${choice.description}` : "";
|
|
380
|
+
return `${choiceIndex + 1}. ${choice.label}${description}`;
|
|
381
|
+
});
|
|
382
|
+
const multipleHint = question.multiple ? "Можно несколько номеров через запятую." : "Можно номер варианта или свой текст.";
|
|
383
|
+
const lines = [`❓ Вопрос ${total > 1 ? `${index + 1}/${total} · ` : ""}${sessionLabel(endpoint)}`];
|
|
384
|
+
if (question.label)
|
|
385
|
+
lines.push(question.label);
|
|
386
|
+
lines.push(question.prompt);
|
|
387
|
+
if (choices.length > 0)
|
|
388
|
+
lines.push("", ...choices);
|
|
389
|
+
lines.push("", `${multipleHint} /cancel — отменить.`);
|
|
390
|
+
return lines.join("\n");
|
|
391
|
+
}
|
|
392
|
+
function sessionLabel(endpoint) {
|
|
393
|
+
const title = endpoint.getTitle().trim() || endpoint.sessionId.slice(0, 8);
|
|
394
|
+
const project = basename(endpoint.cwd) || endpoint.cwd;
|
|
395
|
+
return title === project ? title : `${title} · ${project}`;
|
|
396
|
+
}
|
|
397
|
+
function isCancelCommand(text) {
|
|
398
|
+
return /^\/cancel(?:@[A-Za-z0-9_]+)?$/i.test(text.trim());
|
|
399
|
+
}
|
|
400
|
+
function errorMessage(error) {
|
|
401
|
+
return error instanceof Error ? error.message : String(error);
|
|
402
|
+
}
|
|
403
|
+
const COORDINATOR_KEY = Symbol.for("pix.telegram.connector.coordinator.v1");
|
|
404
|
+
export function getTelegramConnectorCoordinator() {
|
|
405
|
+
const root = globalThis;
|
|
406
|
+
root[COORDINATOR_KEY] ??= new TelegramConnectorCoordinator();
|
|
407
|
+
return root[COORDINATOR_KEY];
|
|
408
|
+
}
|
|
409
|
+
export function resetTelegramConnectorCoordinatorForTests() {
|
|
410
|
+
const root = globalThis;
|
|
411
|
+
root[COORDINATOR_KEY]?.stop();
|
|
412
|
+
delete root[COORDINATOR_KEY];
|
|
413
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { registerRemoteQuestionHandler } from "../question/remote.js";
|
|
2
|
+
import { resolveTelegramConnectorConfig } from "./config.js";
|
|
3
|
+
import { getTelegramConnectorCoordinator } from "./coordinator.js";
|
|
4
|
+
const INTERNAL_NEW_SESSION_COMMAND = "telegram-new-session";
|
|
5
|
+
const SESSION_ABORTED_EVENT = "pix:session-aborted";
|
|
6
|
+
export default function telegramConnector(pi) {
|
|
7
|
+
const config = resolveTelegramConnectorConfig();
|
|
8
|
+
const coordinator = getTelegramConnectorCoordinator();
|
|
9
|
+
let currentCtx;
|
|
10
|
+
let attachedSessionId;
|
|
11
|
+
let attachedEndpoint;
|
|
12
|
+
let unregisterRemoteQuestion;
|
|
13
|
+
let lastAssistantText;
|
|
14
|
+
let lastFailureReason;
|
|
15
|
+
let userAborted = false;
|
|
16
|
+
function attach(ctx) {
|
|
17
|
+
currentCtx = ctx;
|
|
18
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
19
|
+
if (attachedSessionId === sessionId)
|
|
20
|
+
return;
|
|
21
|
+
if (attachedSessionId)
|
|
22
|
+
coordinator.detachSession(attachedSessionId, attachedEndpoint);
|
|
23
|
+
unregisterRemoteQuestion?.();
|
|
24
|
+
attachedSessionId = sessionId;
|
|
25
|
+
const endpoint = {
|
|
26
|
+
sessionId,
|
|
27
|
+
cwd: ctx.cwd,
|
|
28
|
+
getTitle: () => safeSessionTitle(pi, currentCtx, sessionId),
|
|
29
|
+
isIdle: () => safeIsIdle(currentCtx),
|
|
30
|
+
sendUserMessage: (text) => {
|
|
31
|
+
const options = safeIsIdle(currentCtx) ? undefined : { deliverAs: "followUp" };
|
|
32
|
+
pi.sendUserMessage(text, options);
|
|
33
|
+
},
|
|
34
|
+
dispatchNewSession: (requestId) => {
|
|
35
|
+
const options = {
|
|
36
|
+
...(safeIsIdle(currentCtx) ? {} : { deliverAs: "followUp" }),
|
|
37
|
+
expandPromptTemplates: true,
|
|
38
|
+
};
|
|
39
|
+
pi.sendUserMessage(`/${INTERNAL_NEW_SESSION_COMMAND} ${requestId}`, options);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
attachedEndpoint = endpoint;
|
|
43
|
+
coordinator.attachSession(endpoint, config);
|
|
44
|
+
unregisterRemoteQuestion = registerRemoteQuestionHandler(sessionId, (questions, signal) => coordinator.askQuestions(sessionId, questions, signal));
|
|
45
|
+
}
|
|
46
|
+
function detach() {
|
|
47
|
+
unregisterRemoteQuestion?.();
|
|
48
|
+
unregisterRemoteQuestion = undefined;
|
|
49
|
+
if (attachedSessionId)
|
|
50
|
+
coordinator.detachSession(attachedSessionId, attachedEndpoint);
|
|
51
|
+
attachedSessionId = undefined;
|
|
52
|
+
attachedEndpoint = undefined;
|
|
53
|
+
currentCtx = undefined;
|
|
54
|
+
}
|
|
55
|
+
pi.registerCommand("tg", {
|
|
56
|
+
description: "Show Telegram connector status",
|
|
57
|
+
handler: async (_args, ctx) => {
|
|
58
|
+
attach(ctx);
|
|
59
|
+
if (!config.enabled || !config.botToken || !config.chatId) {
|
|
60
|
+
ctx.ui.notify("Telegram connector is not configured. Set telegramConnector.botToken/chatId or PIX_TELEGRAM_* env vars.", "warning");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
ctx.ui.notify("Telegram connector is active for this Pix process.", "info");
|
|
64
|
+
void coordinator.sendStatus().catch(() => { });
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// Internal command dispatched through sendUserMessage(expandPromptTemplates=true).
|
|
68
|
+
// This intentionally obtains a fresh ExtensionCommandContext at execution time,
|
|
69
|
+
// instead of keeping command contexts alive across async Telegram callbacks.
|
|
70
|
+
pi.registerCommand(INTERNAL_NEW_SESSION_COMMAND, {
|
|
71
|
+
description: "Internal Telegram connector session handoff",
|
|
72
|
+
handler: async (args, ctx) => {
|
|
73
|
+
const sourceSessionId = ctx.sessionManager.getSessionId();
|
|
74
|
+
const requestId = args.trim();
|
|
75
|
+
const task = coordinator.consumeNewSessionRequest(requestId, sourceSessionId);
|
|
76
|
+
if (!task)
|
|
77
|
+
return;
|
|
78
|
+
try {
|
|
79
|
+
await ctx.waitForIdle();
|
|
80
|
+
const result = await ctx.newSession({
|
|
81
|
+
withSession: async (nextCtx) => {
|
|
82
|
+
const nextSessionId = nextCtx.sessionManager.getSessionId();
|
|
83
|
+
// Start both operations from the fresh replacement context, but do not
|
|
84
|
+
// make the agent turn wait for Telegram delivery (or vice versa).
|
|
85
|
+
// The Bot API call is started first so the normal case shows the new-
|
|
86
|
+
// session card before that session eventually reports completion.
|
|
87
|
+
void coordinator.announceNewSession(nextSessionId).catch(() => { });
|
|
88
|
+
void nextCtx.sendUserMessage(task, { expandPromptTemplates: false }).catch(() => {
|
|
89
|
+
void coordinator.sendNotice("Новая сессия создана, но задачу запустить не удалось.").catch(() => { });
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
if (result.cancelled) {
|
|
94
|
+
void coordinator.sendNotice("Новая сессия не была создана.").catch(() => { });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
void coordinator.sendNotice("Не удалось создать новую сессию.").catch(() => { });
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
coordinator.finishNewSessionRequest(sourceSessionId);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
106
|
+
attach(ctx);
|
|
107
|
+
});
|
|
108
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
109
|
+
attach(ctx);
|
|
110
|
+
lastAssistantText = undefined;
|
|
111
|
+
lastFailureReason = undefined;
|
|
112
|
+
userAborted = false;
|
|
113
|
+
});
|
|
114
|
+
pi.on("message_end", async (event, ctx) => {
|
|
115
|
+
attach(ctx);
|
|
116
|
+
const text = assistantVisibleText(event.message);
|
|
117
|
+
if (text)
|
|
118
|
+
lastAssistantText = text;
|
|
119
|
+
});
|
|
120
|
+
pi.on("message_update", async (event, ctx) => {
|
|
121
|
+
attach(ctx);
|
|
122
|
+
const update = event.assistantMessageEvent;
|
|
123
|
+
if (update.type === "error" && update.reason === "aborted") {
|
|
124
|
+
userAborted = true;
|
|
125
|
+
lastFailureReason = undefined;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (update.type === "error") {
|
|
129
|
+
const reason = typeof update.error?.errorMessage === "string" ? update.error.errorMessage.trim() : "";
|
|
130
|
+
if (reason)
|
|
131
|
+
lastFailureReason = reason;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (typeof update.type === "string")
|
|
135
|
+
lastFailureReason = undefined;
|
|
136
|
+
});
|
|
137
|
+
pi.events.on(SESSION_ABORTED_EVENT, (data) => {
|
|
138
|
+
if (isRecord(data) && data.aborted === true) {
|
|
139
|
+
userAborted = true;
|
|
140
|
+
lastFailureReason = undefined;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
144
|
+
attach(ctx);
|
|
145
|
+
if (userAborted || !attachedSessionId)
|
|
146
|
+
return;
|
|
147
|
+
const sessionId = attachedSessionId;
|
|
148
|
+
const summary = lastFailureReason ?? lastAssistantText;
|
|
149
|
+
void coordinator.notifyCompletion(sessionId, lastFailureReason ? "error" : "complete", summary).catch(() => {
|
|
150
|
+
// Telegram is an optional notification channel and must never delay or fail the run.
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
pi.on("session_shutdown", async () => {
|
|
154
|
+
detach();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
export function assistantVisibleText(message) {
|
|
158
|
+
if (!isRecord(message) || message.role !== "assistant")
|
|
159
|
+
return undefined;
|
|
160
|
+
const content = message.content;
|
|
161
|
+
if (typeof content === "string")
|
|
162
|
+
return normalizedText(content);
|
|
163
|
+
if (!Array.isArray(content))
|
|
164
|
+
return undefined;
|
|
165
|
+
const text = content.flatMap((part) => {
|
|
166
|
+
if (!isRecord(part) || part.type !== "text" || typeof part.text !== "string")
|
|
167
|
+
return [];
|
|
168
|
+
return [part.text];
|
|
169
|
+
}).join("\n");
|
|
170
|
+
return normalizedText(text);
|
|
171
|
+
}
|
|
172
|
+
function safeSessionTitle(pi, ctx, sessionId) {
|
|
173
|
+
try {
|
|
174
|
+
const title = pi.getSessionName()?.trim() || ctx?.sessionManager.getSessionName?.()?.trim();
|
|
175
|
+
return title || sessionId.slice(0, 8);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return sessionId.slice(0, 8);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function safeIsIdle(ctx) {
|
|
182
|
+
try {
|
|
183
|
+
return ctx?.isIdle() ?? false;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function normalizedText(text) {
|
|
190
|
+
const normalized = text.trim();
|
|
191
|
+
return normalized || undefined;
|
|
192
|
+
}
|
|
193
|
+
function isRecord(value) {
|
|
194
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
195
|
+
}
|