Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions docs/specs/telegram-remote-control/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Implementation Plan

## Architecture

- Add `src/main/presenter/remoteControlPresenter/` as a main-process presenter that exposes a small shared contract to the renderer through the existing `presenter:call` IPC path.
- Keep Telegram transport in Electron main using native `fetch` and Bot API long polling.
- Reuse `newAgentPresenter.sendMessage()` and `DeepChatAgentPresenter` for message persistence, stream state, title generation, and stop behavior.
- Add detached session creation to `newAgentPresenter` so remote conversations do not require a renderer-bound window.

## Main-Process Modules

- `remoteBindingStore`
- Stores `remoteControl.telegram` config in Electron Store.
- Persists poll offset, allowlist, default agent id, pair code, internal stream mode, and endpoint bindings.
- Keeps active event IDs, `/sessions` snapshots, and `/model` inline-menu state in memory.
- `remoteAuthGuard`
- Enforces private-chat-only usage.
- Authenticates strictly by numeric `from.id`.
- Supports one-time `/pair <code>` flow.
- `remoteConversationRunner`
- Creates detached sessions when needed.
- Resolves a valid enabled DeepChat default agent before creating unbound Telegram sessions.
- Lists recent sessions by the currently bound session's agent when a valid binding exists; otherwise falls back to the default DeepChat agent.
- Exposes current-session lookup and bound-session model switching through `newAgentPresenter.setSessionModel()`.
- Reuses `newAgentPresenter.sendMessage()` for plain-text Telegram input.
- Tracks the active assistant message/event for `/stop`.
- `remoteCommandRouter`
- Handles `/start`, `/help`, `/pair`, `/new`, `/sessions`, `/use`, `/stop`, `/status`, `/model`, plain text, and `/model` callback actions.
- `telegramClient`
- Calls `getMe`, `getUpdates`, `sendMessageDraft`, `sendMessage`, `sendChatAction`, `setMyCommands`, `setMessageReaction`, `editMessageText`, `editMessageReplyMarkup`, and `answerCallbackQuery`.
- `telegramParser`
- Parses private text updates, bot commands, and callback queries into one internal event shape.
- `telegramOutbound`
- Builds plain-text assistant output, detects “desktop confirmation required” states, and chunks output to 4096 characters.
- `telegramPoller`
- Runs a single sequential long-poll loop.
- Advances the stored offset only after a specific update is handled successfully.
- Uses exponential backoff on failures.
- Only adds reactions for plain-text conversation messages and clears them after the conversation completes or fails.

## Shared / IPC Contract

- Add `src/shared/types/presenters/remote-control.presenter.d.ts`.
- Expose methods for reading/saving Telegram settings, reading runtime status, listing/removing bindings, reading pairing snapshot, generating/clearing pair codes, clearing bindings, and testing Telegram hooks.

## Renderer Plan

- Add a new `Remote` settings route and `RemoteSettings.vue`.
- Move Telegram configuration out of `NotificationsHooksSettings.vue`.
- Keep `Hooks` for Discord, Confirmo, and command hooks only.
- Simplify the first-layer Telegram remote UI to allowed user IDs, default agent selection, pairing, and binding management.
- Show pairing and binding management inside dialogs; hide remote/hook detail forms when their toggle is off.
- Reuse existing i18n flow for all renderer-visible strings.

## Data Model

- SQLite
- No schema change.
- Sessions/messages continue to use existing new-agent tables.
- Electron Store
- `hooksNotifications.telegram`
- Shared Telegram bot token and hook notification target settings.
- `remoteControl.telegram`
- `enabled`
- `allowlist`
- `defaultAgentId`
- `streamMode`
- `pairing`
- `pollOffset`
- `bindings`

## Event / Request Flow

1. Renderer saves Remote settings through `remoteControlPresenter`.
2. Main presenter updates `hooksNotifications.telegram` and `remoteControl.telegram`, then rebuilds the Telegram runtime if required.
3. Telegram poller receives private updates through `getUpdates`.
4. Parser normalizes message and callback payloads.
5. Router applies auth, command handling, and `/model` inline-menu transitions.
6. Plain text enters `newAgentPresenter.sendMessage()` using the bound or newly created detached session.
7. `/model` callback actions edit a single bot menu message in place and answer the callback query.
8. Poller watches assistant message state and sends draft/final Telegram output.
9. If the assistant pauses on a permission/question action, Telegram returns a desktop-confirmation notice instead of bypassing approval.

## Testing Strategy

- Unit tests for `remoteAuthGuard`.
- Unit tests for `remoteBindingStore`.
- Unit tests for `remoteCommandRouter`.
- Unit tests for `remoteConversationRunner`.
- Unit tests for `telegramParser`.
- Unit tests for `telegramClient` request payloads.
- Unit tests for `telegramOutbound` chunking/final-text behavior.
- Unit tests for Telegram command registration, callback handling, and message reaction lifecycle behavior.
- Presenter-level tests for detached session creation.
- Presenter-level tests for stop-by-event behavior.

## Migration Note

- No SQLite migration is required.
- Existing Telegram hook settings remain compatible.
- New remote state is additive and can be removed cleanly by disabling remote control or clearing the config blob.
59 changes: 59 additions & 0 deletions docs/specs/telegram-remote-control/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Telegram Remote Control

## Summary

Add Telegram private-chat remote control to the `dev` branch without changing DeepChat's main architecture. The bot runtime lives in Electron main, remote messages reuse the existing DeepChat session/message/stream pipeline, and the settings UI moves Telegram-related controls into a new `Remote` page.

This increment simplifies the Remote settings UX, removes the user-facing stream mode switch, adds a selectable default DeepChat agent for new remote sessions, and keeps remote session/model control intentionally lightweight for v1.

## User Stories

- As a DeepChat desktop user, I can pair my Telegram account and send a DM to my bot to continue a DeepChat conversation remotely.
- As a paired user, my first Telegram DM can create a detached DeepChat session even when no chat window is focused.
- As a paired user, I can stop an active generation with `/stop`, list recent sessions with `/sessions`, and rebind to one with `/use`.
- As a user configuring integrations, I can manage Telegram remote control and Telegram hook notifications from a single `Remote` settings page.
- As a user configuring Telegram pairing, I only see a simple “Pair” entry point in the first layer and complete the flow from a small modal.
- As a user using multiple DeepChat agents, I can choose which enabled DeepChat agent new Telegram sessions should use by default.
- As a paired user, I can change the current bound session's provider/model through a two-step Telegram inline keyboard opened from `/model`.

## Acceptance Criteria

- An authorized Telegram private-chat user can DM the bot and receive a DeepChat assistant reply.
- The first DM can create a detached DeepChat session without a focused window or existing `webContents` binding.
- Subsequent DMs continue the currently bound DeepChat session for that Telegram endpoint.
- `/stop` cancels the active generation for that remote endpoint through the existing stop path.
- `/sessions` lists recent sessions for the currently bound session's agent; if no valid binding exists, it falls back to the configured default DeepChat agent.
- `/use <index>` binds the endpoint to the corresponding session from the latest `/sessions` list.
- `/model` opens a Telegram inline keyboard, first for enabled providers and then for enabled models, and only changes the current bound session.
- Remote-triggered conversations do not bypass the existing permission flow for tools, files, or settings.
- Telegram settings appear under a new `Remote` settings page, and the old Telegram section is removed from `Hooks`.
- The Remote settings page hides the remote-control detail area when remote control is disabled, and hides the Telegram hook detail area when hooks are disabled.
- The first-layer Telegram remote UI shows allowed user IDs, a default DeepChat agent selector, a pairing button, and a binding-management button; pair code display moves into a modal.
- Telegram remote no longer exposes a stream mode selector; draft streaming remains the internal default.
- Telegram runtime registers its default command list when it starts.
- Only plain-text conversation messages get a temporary bot reaction; slash commands and inline-button callbacks do not, and the reaction is cleared after the reply finishes or fails.
- New Telegram sessions use the selected default DeepChat agent, inheriting that agent's default model/project/permission defaults; existing bound sessions remain bound even if the default agent later changes.
- Existing local desktop chat behavior remains unchanged.

## Constraints

- Telegram only for v1.
- No generic channel registry or plugin system.
- Bot runtime lives in Electron main, not renderer or preload.
- SQLite remains the source of truth for sessions and messages.
- Config/state uses the existing Electron Store path; no new SQLite migration is introduced.
- v1 is private-chat only. No group support, no media upload, no remote permission approvals.

## Non-Goals

- Group chats, forum moderation, or multi-platform messaging channels.
- Telegram media uploads, arbitrary bot button workflows outside `/model`, or Markdown/HTML rich formatting.
- Remote approval of tool/file/settings permission requests.
- A standalone helper daemon or public remote-control SDK.

## Compatibility

- Existing Telegram hook settings remain valid and are reused by the new `Remote` page.
- New remote-specific state lives under `remoteControl.telegram` in Electron Store.
- `remoteControl.telegram.defaultAgentId` stores the default DeepChat agent for new Telegram sessions.
- Disabling remote control or clearing the bot token cleanly stops polling without affecting local chats or persisted SQLite data.
36 changes: 36 additions & 0 deletions docs/specs/telegram-remote-control/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Tasks

1. Main presenter
- Add `remoteControlPresenter` contract and register it in main `Presenter`.
- Rebuild runtime on settings changes and app init.

2. Detached session support
- Add `createDetachedSession()` to `newAgentPresenter`.
- Ensure first remote message still triggers title generation through the shared send path.

3. Remote runtime services
- Implement auth guard, binding store, command router, and conversation runner.
- Route new Telegram sessions through a validated default DeepChat agent.
- Make `/sessions` prefer the currently bound agent and add bound-session `/model` switching.

4. Telegram transport
- Implement native-fetch Telegram client.
- Implement long polling with offset persistence and backoff.
- Implement plain-text outbound chunking and draft/final delivery.
- Register default Telegram bot commands, support inline-keyboard callback queries, and keep reactions scoped to plain-text conversations.

5. Renderer
- Add `RemoteSettings.vue`.
- Add `settings-remote` route.
- Remove Telegram UI from `NotificationsHooksSettings.vue`.
- Simplify the Telegram remote first layer and move pairing / binding management into dialogs.
- Hide remote and hook detail sections when their toggle is off.
- Add i18n keys for `Remote`.

6. Tests
- Add main tests for auth guard, bindings, command routing, and chunking.
- Add parser/client tests for callback query and inline-keyboard payloads.
- Extend existing presenter tests for detached session creation, session model switching, and stop-by-event behavior.

7. Validation
- Run formatting, i18n check, lint, and targeted tests when dependencies are available in the worktree.
3 changes: 1 addition & 2 deletions src/main/presenter/configPresenter/systemPromptHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ type SetSetting = <T>(key: string, value: T) => void

export const DEFAULT_SYSTEM_PROMPT = `You are DeepChat, a highly capable AI assistant. Your goal is to fully complete the user’s requested task before handing the conversation back to them. Keep working autonomously until the task is fully resolved.
Be thorough in gathering information. Before replying, make sure you have all the details necessary to provide a complete solution. Use additional tools or ask clarifying questions when needed, but if you can find the answer on your own, avoid asking the user for help.
When using tools, briefly describe your intended steps first—for example, which tool you’ll use and for what purpose.
Adhere to this in all languages.Respond in the same language as the user's query.`
When using tools, briefly describe your intended steps first—for example, which tool you’ll use and for what purpose.`

type GetSetting = <T>(key: string) => T | undefined

Expand Down
22 changes: 22 additions & 0 deletions src/main/presenter/deepchatAgentPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,28 @@ export class DeepChatAgentPresenter implements IAgentImplementation {
this.setSessionStatus(sessionId, 'idle')
}

getActiveGeneration(sessionId: string): { eventId: string; runId: string } | null {
const activeGeneration = this.activeGenerations.get(sessionId)
if (!activeGeneration) {
return null
}

return {
eventId: activeGeneration.messageId,
runId: activeGeneration.runId
}
}

async cancelGenerationByEventId(sessionId: string, eventId: string): Promise<boolean> {
const activeGeneration = this.activeGenerations.get(sessionId)
if (!activeGeneration || activeGeneration.messageId !== eventId) {
return false
}

await this.cancelGeneration(sessionId)
return true
}

private dispatchTerminalHooks(
sessionId: string,
state: DeepChatSessionState | undefined,
Expand Down
Loading
Loading