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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,19 @@ Set these environment variables before starting `codexapp`:

```bash
export TELEGRAM_BOT_TOKEN="<your-telegram-bot-token>"
export TELEGRAM_ALLOWED_USER_IDS="<your-telegram-user-id>,<optional-second-id>"
export TELEGRAM_DEFAULT_CWD="$PWD" # optional, defaults to current working directory
npx codexapp
```

`TELEGRAM_ALLOWED_USER_IDS` is required for safe access. Only allowlisted Telegram user IDs can use the bridge. If no allowed user IDs are configured, incoming Telegram messages are rejected.

To find your Telegram user ID:

1. Send a message to your bot.
2. Run `curl "https://api.telegram.org/bot<your-telegram-bot-token>/getUpdates"`.
3. Read `message.from.id` from the returned update payload.

Bot commands:

- `/newthread` create and map a new Codex thread for this Telegram chat
Expand Down
224 changes: 200 additions & 24 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,19 @@
@export-thread="onExportThread" />
</div>

<div v-if="!isSidebarCollapsed" class="sidebar-settings-area">
<div
v-if="!isSidebarCollapsed"
ref="settingsAreaRef"
class="sidebar-settings-area"
@click="onSettingsAreaClick"
>
<Transition name="settings-panel">
<div v-if="isSettingsOpen" class="sidebar-settings-panel">
<div
v-if="isSettingsOpen"
ref="settingsPanelRef"
class="sidebar-settings-panel"
@click.stop
>
<div class="sidebar-settings-account-section">
<div class="sidebar-settings-account-header">
<div class="sidebar-settings-account-header-main">
Expand Down Expand Up @@ -195,10 +205,49 @@
@update:model-value="onDictationLanguageChange"
/>
</div>
<button class="sidebar-settings-row" type="button" aria-live="polite" @click="onConnectTelegramBot">
<button class="sidebar-settings-row" type="button" aria-live="polite" @click="isTelegramConfigOpen = !isTelegramConfigOpen">
<span class="sidebar-settings-label">Telegram</span>
<span class="sidebar-settings-value">{{ telegramStatusText }}</span>
</button>
<div v-if="isTelegramConfigOpen" class="sidebar-settings-telegram-panel">
<label class="sidebar-settings-field">
<span class="sidebar-settings-field-label">Bot token</span>
<input
v-model="telegramBotTokenDraft"
class="sidebar-settings-input"
type="password"
placeholder="123456:ABCDEF"
autocomplete="off"
spellcheck="false"
>
</label>
<label class="sidebar-settings-field">
<span class="sidebar-settings-field-label">Allowed Telegram user IDs</span>
<textarea
v-model="telegramAllowedUserIdsDraft"
class="sidebar-settings-textarea"
rows="3"
placeholder="123456789&#10;987654321"
spellcheck="false"
/>
</label>
<div class="sidebar-settings-field-help">
Put one Telegram user ID per line or separate them with commas. Use `*` to allow all Telegram users. Unauthorized users will see their own ID in the rejection message so they can copy it here.
</div>
<div v-if="telegramConfigError" class="sidebar-settings-telegram-error">
{{ telegramConfigError }}
</div>
<div class="sidebar-settings-telegram-actions">
<button
class="sidebar-settings-telegram-save"
type="button"
:disabled="isTelegramSaving"
@click="saveTelegramConfig"
>
{{ isTelegramSaving ? 'Saving…' : 'Save Telegram config' }}
</button>
</div>
</div>
<div
v-if="showThreadContextBadge"
class="sidebar-settings-row sidebar-settings-context-row"
Expand All @@ -219,7 +268,12 @@
</div>
</div>
</Transition>
<button class="sidebar-settings-button" type="button" @click="isSettingsOpen = !isSettingsOpen">
<button
ref="settingsButtonRef"
class="sidebar-settings-button"
type="button"
@click.stop="isSettingsOpen = !isSettingsOpen"
>
<IconTablerSettings class="sidebar-settings-icon" />
<span>Settings</span>
<span class="sidebar-settings-button-version">
Expand Down Expand Up @@ -485,6 +539,7 @@
:thread-token-usage="selectedThreadTokenUsage"
:codex-quota="codexQuota"
:is-turn-in-progress="false"
:is-stop-pending="false"
:is-interrupting-turn="false" :send-with-enter="sendWithEnter" :in-progress-submit-mode="inProgressSendMode"
:dictation-click-to-toggle="dictationClickToToggle" :dictation-auto-send="dictationAutoSend"
:dictation-language="dictationLanguage"
Expand Down Expand Up @@ -544,7 +599,9 @@
:skills="installedSkills"
:thread-token-usage="selectedThreadTokenUsage"
:codex-quota="codexQuota"
:is-turn-in-progress="isSelectedThreadInProgress" :is-interrupting-turn="isInterruptingTurn"
:is-turn-in-progress="isSelectedThreadInProgress"
:is-stop-pending="isSelectedThreadInterruptPending"
:is-interrupting-turn="isInterruptingTurn"
:has-queue-above="selectedThreadQueuedMessages.length > 0"
:send-with-enter="sendWithEnter" :in-progress-submit-mode="inProgressSendMode"
:dictation-click-to-toggle="dictationClickToToggle" :dictation-auto-send="dictationAutoSend"
Expand Down Expand Up @@ -592,6 +649,7 @@ import {
getAccounts,
createLocalDirectory,
getHomeDirectory,
getTelegramConfig,
getProjectRootSuggestion,
getTelegramStatus,
getWorkspaceRootsState,
Expand Down Expand Up @@ -779,6 +837,7 @@ const {
isLoadingMessages,
isSendingMessage,
isInterruptingTurn,
isSelectedThreadInterruptPending,
isUpdatingSpeedMode,
refreshAll,
refreshSkills,
Expand Down Expand Up @@ -839,6 +898,9 @@ const isSidebarCollapsed = ref(loadSidebarCollapsed())
const sidebarSearchQuery = ref('')
const isSidebarSearchVisible = ref(false)
const sidebarSearchInputRef = ref<HTMLInputElement | null>(null)
const settingsAreaRef = ref<HTMLElement | null>(null)
const settingsPanelRef = ref<HTMLElement | null>(null)
const settingsButtonRef = ref<HTMLElement | null>(null)
const serverMatchedThreadIds = ref<string[] | null>(null)
let threadSearchTimer: ReturnType<typeof setTimeout> | null = null
const defaultNewProjectName = ref('New Project (1)')
Expand Down Expand Up @@ -878,6 +940,11 @@ const dictationLanguage = ref(loadDictationLanguagePref())
const dictationLanguageOptions = computed(() => buildDictationLanguageOptions())

const showGithubTrendingProjects = ref(loadBoolPref(GITHUB_TRENDING_PROJECTS_KEY, false))
const isTelegramConfigOpen = ref(false)
const telegramBotTokenDraft = ref('')
const telegramAllowedUserIdsDraft = ref('')
const telegramConfigError = ref('')
const isTelegramSaving = ref(false)
const isCreateFolderOpen = ref(false)
const createFolderDraft = ref('')
const createFolderError = ref('')
Expand All @@ -896,6 +963,8 @@ const telegramStatus = ref<TelegramStatus>({
active: false,
mappedChats: 0,
mappedThreads: 0,
allowedUsers: 0,
allowAllUsers: false,
lastError: '',
})
const mobileHiddenAtMs = ref<number | null>(null)
Expand Down Expand Up @@ -1164,12 +1233,16 @@ const contentStyle = computed(() => {
const telegramStatusText = computed(() => {
if (!telegramStatus.value.configured) return 'Not configured'
const base = telegramStatus.value.active ? 'Online' : 'Configured (offline)'
const mapped = `${telegramStatus.value.mappedChats} chat(s), ${telegramStatus.value.mappedThreads} thread(s)`
const allowlist = telegramStatus.value.allowAllUsers
? 'allow all users'
: `${telegramStatus.value.allowedUsers} allowed user(s)`
const mapped = `${telegramStatus.value.mappedChats} chat(s), ${telegramStatus.value.mappedThreads} thread(s), ${allowlist}`
const error = telegramStatus.value.lastError ? `, error: ${telegramStatus.value.lastError}` : ''
return `${base}, ${mapped}${error}`
})

onMounted(() => {
document.addEventListener('pointerdown', onDocumentPointerDown)
window.addEventListener('keydown', onWindowKeyDown)
document.addEventListener('visibilitychange', onDocumentVisibilityChange)
window.addEventListener('pageshow', onWindowPageShow)
Expand All @@ -1180,13 +1253,15 @@ onMounted(() => {
void loadHomeDirectory()
void loadWorkspaceRootOptionsState()
void refreshDefaultProjectName()
void refreshTelegramConfig()
void refreshTelegramStatus()
if (showGithubTrendingProjects.value) {
void loadTrendingProjects()
}
})

onUnmounted(() => {
document.removeEventListener('pointerdown', onDocumentPointerDown)
window.removeEventListener('keydown', onWindowKeyDown)
document.removeEventListener('visibilitychange', onDocumentVisibilityChange)
window.removeEventListener('pageshow', onWindowPageShow)
Expand Down Expand Up @@ -1261,11 +1336,66 @@ async function refreshTelegramStatus(): Promise<void> {
active: false,
mappedChats: 0,
mappedThreads: 0,
allowedUsers: 0,
allowAllUsers: false,
lastError: message,
}
}
}

async function refreshTelegramConfig(): Promise<void> {
try {
const config = await getTelegramConfig()
telegramBotTokenDraft.value = config.botToken
telegramAllowedUserIdsDraft.value = config.allowedUserIds.map((value) => String(value)).join('\n')
telegramConfigError.value = ''
} catch (error) {
telegramConfigError.value = error instanceof Error ? error.message : 'Failed to load Telegram configuration'
}
}

function parseTelegramAllowedUserIdsInput(value: string): Array<number | '*'> {
const rawEntries = value
.split(/[\n,]/)
.map((entry) => entry.trim().replace(/^(telegram|tg):/i, '').trim())
.filter(Boolean)
const allowAllUsers = rawEntries.includes('*')
const normalizedUserIds = Array.from(new Set(rawEntries
.filter((entry) => /^-?\d+$/.test(entry))
.map((entry) => Number.parseInt(entry, 10))))
return allowAllUsers ? ['*', ...normalizedUserIds] : normalizedUserIds
}

async function saveTelegramConfig(): Promise<void> {
const botToken = telegramBotTokenDraft.value.trim()
const allowedUserIds = parseTelegramAllowedUserIdsInput(telegramAllowedUserIdsDraft.value)
if (!botToken) {
telegramConfigError.value = 'Telegram bot token is required.'
return
}
if (allowedUserIds.length === 0) {
telegramConfigError.value = 'At least one allowed Telegram user ID or * is required.'
return
}

isTelegramSaving.value = true
telegramConfigError.value = ''
try {
await configureTelegramBot(botToken, allowedUserIds)
telegramAllowedUserIdsDraft.value = allowedUserIds.map((value) => String(value)).join('\n')
await Promise.all([
refreshTelegramConfig(),
refreshTelegramStatus(),
])
window.alert('Telegram bot configured. Only allowlisted Telegram users can use the bridge.')
} catch (error) {
telegramConfigError.value = error instanceof Error ? error.message : 'Failed to connect Telegram bot'
void refreshTelegramStatus()
} finally {
isTelegramSaving.value = false
}
}

function toggleSidebarSearch(): void {
isSidebarSearchVisible.value = !isSidebarSearchVisible.value
if (isSidebarSearchVisible.value) {
Expand Down Expand Up @@ -1656,13 +1786,35 @@ function setSidebarCollapsed(nextValue: boolean): void {

function onWindowKeyDown(event: KeyboardEvent): void {
if (event.defaultPrevented) return
if (event.key === 'Escape' && isSettingsOpen.value) {
isSettingsOpen.value = false
return
}
if (!event.ctrlKey && !event.metaKey) return
if (event.shiftKey || event.altKey) return
if (event.key.toLowerCase() !== 'b') return
event.preventDefault()
setSidebarCollapsed(!isSidebarCollapsed.value)
}

function onDocumentPointerDown(event: PointerEvent): void {
if (!isSettingsOpen.value) return
const target = event.target
if (!(target instanceof Node)) return
if (settingsPanelRef.value?.contains(target)) return
if (settingsButtonRef.value?.contains(target)) return
isSettingsOpen.value = false
}

function onSettingsAreaClick(event: MouseEvent): void {
if (!isSettingsOpen.value) return
const target = event.target
if (!(target instanceof Node)) return
if (settingsPanelRef.value?.contains(target)) return
if (settingsButtonRef.value?.contains(target)) return
isSettingsOpen.value = false
}
Comment on lines +1800 to +1816
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. tests.md missing settings-panel test 📘 Rule violation ⚙ Maintainability

The PR changes Settings panel behavior (scrollable panel plus close-on-outside-click/Escape) but
does not add corresponding manual test instructions in tests.md. This violates the requirement to
document manual tests for each implemented feature change.
Agent Prompt
## Issue description
`tests.md` is missing manual test instructions to verify the updated Settings panel usability (scrolling and closing by outside click / Escape).

## Issue Context
The PR adds new close-handlers and scroll styling for the Settings panel, which needs repeatable manual verification steps.

## Fix Focus Areas
- tests.md[1-40]
- src/App.vue[1787-1816]
- src/App.vue[3362-3364]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


function onDocumentVisibilityChange(): void {
if (typeof document === 'undefined') return
if (!isMobile.value) return
Expand Down Expand Up @@ -1757,23 +1909,6 @@ function onGithubTipsScopeChange(nextValue: string): void {
githubTipsScope.value = scope
}

function onConnectTelegramBot(): void {
if (typeof window === 'undefined') return
const botToken = window.prompt('Telegram bot token')
if (!botToken || !botToken.trim()) return

void configureTelegramBot(botToken.trim())
.then(() => {
window.alert('Telegram bot configured. Open the bot DM and send /start.')
void refreshTelegramStatus()
})
.catch((error: unknown) => {
const message = error instanceof Error ? error.message : 'Failed to connect Telegram bot'
window.alert(message)
void refreshTelegramStatus()
})
}

function onSelectTrendingProjectTip(project: GithubTrendingProject): void {
const composer = homeThreadComposerRef.value
if (!composer) return
Expand Down Expand Up @@ -3225,7 +3360,7 @@ async function loadWorktreeBranches(sourceCwd: string): Promise<void> {
}

.sidebar-settings-panel {
@apply mb-1 rounded-lg border border-zinc-200 bg-white overflow-hidden;
@apply mb-1 max-h-[min(70vh,36rem)] overflow-y-auto rounded-lg border border-zinc-200 bg-white;
}

.sidebar-settings-row {
Expand All @@ -3252,6 +3387,47 @@ async function loadWorktreeBranches(sourceCwd: string): Promise<void> {
@apply border-t border-zinc-100;
}

.sidebar-settings-telegram-panel {
@apply border-t border-zinc-100 bg-zinc-50/70 px-3 py-3;
}

.sidebar-settings-field {
@apply flex flex-col gap-1.5;
}

.sidebar-settings-field + .sidebar-settings-field {
@apply mt-3;
}

.sidebar-settings-field-label {
@apply text-xs font-medium text-zinc-700;
}

.sidebar-settings-input,
.sidebar-settings-textarea {
@apply w-full rounded-md border border-zinc-200 bg-white px-2.5 py-2 text-sm text-zinc-800 outline-none transition focus:border-zinc-400 focus:ring-2 focus:ring-zinc-200;
}

.sidebar-settings-textarea {
@apply min-h-20 resize-y font-mono text-xs;
}

.sidebar-settings-field-help {
@apply mt-2 text-xs leading-5 text-zinc-500;
}

.sidebar-settings-telegram-error {
@apply mt-2 rounded-md bg-rose-50 px-2.5 py-2 text-xs text-rose-700;
}

.sidebar-settings-telegram-actions {
@apply mt-3 flex items-center justify-end;
}

.sidebar-settings-telegram-save {
@apply rounded-full border border-zinc-200 bg-white px-3 py-1.5 text-xs font-medium text-zinc-700 transition hover:bg-zinc-50 disabled:cursor-default disabled:opacity-60;
}

.sidebar-settings-account-section {
@apply border-t border-zinc-100 bg-zinc-50/60 px-3 py-3;
}
Expand Down
Loading