@pixelbyte-software/pixcode 1.36.2 → 1.36.4

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.
Files changed (41) hide show
  1. package/dist/assets/index-CgF0-_6Z.css +32 -0
  2. package/dist/assets/{index-B1PlYTuj.js → index-D-YjltED.js} +182 -182
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/daemon-manager.js +18 -12
  5. package/dist-server/server/daemon-manager.js.map +1 -1
  6. package/dist-server/server/database/db.js +49 -0
  7. package/dist-server/server/database/db.js.map +1 -1
  8. package/dist-server/server/index.js +8 -4
  9. package/dist-server/server/index.js.map +1 -1
  10. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +17 -3
  11. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  12. package/dist-server/server/routes/telegram.js +16 -2
  13. package/dist-server/server/routes/telegram.js.map +1 -1
  14. package/dist-server/server/services/telegram/bot.js +48 -6
  15. package/dist-server/server/services/telegram/bot.js.map +1 -1
  16. package/dist-server/server/services/telegram/control-center.js +761 -0
  17. package/dist-server/server/services/telegram/control-center.js.map +1 -0
  18. package/dist-server/server/services/telegram/telegram-http-client.js +26 -4
  19. package/dist-server/server/services/telegram/telegram-http-client.js.map +1 -1
  20. package/dist-server/server/services/telegram/translations.js +138 -2
  21. package/dist-server/server/services/telegram/translations.js.map +1 -1
  22. package/package.json +5 -1
  23. package/scripts/smoke/chat-session-state.mjs +19 -0
  24. package/scripts/smoke/daemon-entrypoint.mjs +20 -0
  25. package/scripts/smoke/orchestration-user-facing-output.mjs +25 -0
  26. package/scripts/smoke/shell-manual-disconnect.mjs +30 -0
  27. package/scripts/smoke/side-panel-editor-layout.mjs +34 -0
  28. package/scripts/smoke/static-root-routing.mjs +21 -0
  29. package/scripts/smoke/telegram-control.mjs +242 -0
  30. package/scripts/smoke/update-ux.mjs +55 -0
  31. package/scripts/smoke/version-modal-autoshow.mjs +29 -0
  32. package/server/daemon-manager.js +17 -12
  33. package/server/database/db.js +52 -0
  34. package/server/index.js +9 -5
  35. package/server/modules/orchestration/workflows/workflow-runner.ts +18 -3
  36. package/server/routes/telegram.js +17 -2
  37. package/server/services/telegram/bot.js +58 -6
  38. package/server/services/telegram/control-center.js +814 -0
  39. package/server/services/telegram/telegram-http-client.js +25 -4
  40. package/server/services/telegram/translations.js +138 -2
  41. package/dist/assets/index-Dx7QyTSN.css +0 -32
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { mkdirSync, readFileSync, rmSync } from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const runtimeDir = path.resolve('.pixcode-dev', 'smoke-telegram-control');
8
+ mkdirSync(runtimeDir, { recursive: true });
9
+ process.env.DATABASE_PATH = path.join(runtimeDir, 'auth.db');
10
+
11
+ const checks = [
12
+ {
13
+ name: 'telegram bot wires the remote control center and callback queries',
14
+ file: 'server/services/telegram/bot.js',
15
+ test: (source) => (
16
+ source.includes('handleTelegramControlMessage')
17
+ && source.includes('handleTelegramControlCallback')
18
+ && source.includes("bot.on('callback_query'")
19
+ ),
20
+ },
21
+ {
22
+ name: 'telegram HTTP client polls callback queries and can answer them',
23
+ file: 'server/services/telegram/telegram-http-client.js',
24
+ test: (source) => (
25
+ source.includes("allowed_updates: ['message', 'callback_query']")
26
+ && source.includes('answerCallbackQuery')
27
+ && source.includes('editMessageText')
28
+ && source.includes("this.emit('callback_query'")
29
+ ),
30
+ },
31
+ {
32
+ name: 'telegram control center exposes provider, model, workflow, install, and settings actions',
33
+ file: 'server/services/telegram/control-center.js',
34
+ test: (source) => (
35
+ source.includes('showMainMenu')
36
+ && source.includes('showProviderMenu')
37
+ && source.includes('showModelMenu')
38
+ && source.includes('showWorkflowMenu')
39
+ && source.includes('runWorkflow')
40
+ && source.includes('startCliInstall')
41
+ && source.includes('updateTelegramControlState')
42
+ && source.includes('/api/agent')
43
+ && source.includes('/api/orchestration/workflows')
44
+ ),
45
+ },
46
+ {
47
+ name: 'telegram link state persists remote-control preferences',
48
+ file: 'server/database/db.js',
49
+ test: (source) => (
50
+ source.includes('telegram_control')
51
+ && source.includes('getControlState')
52
+ && source.includes('updateControlState')
53
+ && source.includes('remoteControlEnabled')
54
+ ),
55
+ },
56
+ {
57
+ name: 'telegram settings UI exposes remote-control toggles',
58
+ file: 'src/components/settings/view/tabs/telegram-settings/TelegramSettingsTab.tsx',
59
+ test: (source) => (
60
+ source.includes('controlEnabled')
61
+ && source.includes('progressMode')
62
+ && source.includes('telegram.control.title')
63
+ && source.includes('telegram.control.progressMode')
64
+ ),
65
+ },
66
+ ];
67
+
68
+ const failures = [];
69
+
70
+ for (const check of checks) {
71
+ let source = '';
72
+ try {
73
+ source = readFileSync(check.file, 'utf8');
74
+ } catch {
75
+ failures.push(`${check.name} (${check.file} missing)`);
76
+ continue;
77
+ }
78
+
79
+ if (!check.test(source)) failures.push(check.name);
80
+ }
81
+
82
+ try {
83
+ const {
84
+ handleTelegramControlCallback,
85
+ handleTelegramControlMessage,
86
+ } = await import('../../server/services/telegram/control-center.js');
87
+ const {
88
+ handleIncomingTelegramMessage,
89
+ setTelegramBotForTesting,
90
+ } = await import('../../server/services/telegram/bot.js');
91
+ const { telegramLinksDb } = await import('../../server/database/db.js');
92
+
93
+ const userId = 4242;
94
+ telegramLinksDb.unlink(userId);
95
+ telegramLinksDb.setPairingCode(userId, '123456', new Date(Date.now() + 600_000).toISOString(), 'tr');
96
+ telegramLinksDb.verify(userId, 'chat-4242', 'ali');
97
+
98
+ const sent = [];
99
+ const bot = {
100
+ sendMessage: async (chatId, text, extra = {}) => {
101
+ sent.push({ chatId, text, extra });
102
+ return { ok: true };
103
+ },
104
+ };
105
+
106
+ const link = telegramLinksDb.getByUserId(userId);
107
+ const expectReply = async (input, expectedFragment) => {
108
+ sent.length = 0;
109
+ const handled = await handleTelegramControlMessage({
110
+ bot,
111
+ msg: { chat: { id: 4242 }, text: input },
112
+ link,
113
+ });
114
+ assert.equal(handled, true, `${input} should be handled`);
115
+ assert.ok(sent.length > 0, `${input} should send at least one reply`);
116
+ assert.ok(
117
+ sent[0].text.includes(expectedFragment),
118
+ `${input} should include "${expectedFragment}" but got "${sent[0].text}"`,
119
+ );
120
+ };
121
+
122
+ await expectReply('/start', 'Pixcode Telegram kontrol merkezi');
123
+ await expectReply('/help', 'Komutlar:');
124
+ await expectReply('/start@Otobot', 'Pixcode Telegram kontrol merkezi');
125
+ await expectReply('/help@Otobot', 'Komutlar:');
126
+ await expectReply('/', 'Komutlar:');
127
+
128
+ const menuEvents = [];
129
+ const menuBot = {
130
+ sendMessage: async (chatId, text, extra = {}) => {
131
+ menuEvents.push({ type: 'send', chatId, text, extra });
132
+ return { ok: true, message_id: 77 };
133
+ },
134
+ editMessageText: async (text, extra = {}) => {
135
+ menuEvents.push({ type: 'edit', text, extra });
136
+ return { ok: true, message_id: extra.message_id };
137
+ },
138
+ answerCallbackQuery: async () => ({ ok: true }),
139
+ };
140
+ const findButton = (markup, predicate) => {
141
+ for (const row of markup?.inline_keyboard || []) {
142
+ for (const candidate of row) {
143
+ if (predicate(candidate)) return candidate;
144
+ }
145
+ }
146
+ return null;
147
+ };
148
+
149
+ menuEvents.length = 0;
150
+ await handleTelegramControlMessage({
151
+ bot: menuBot,
152
+ msg: { chat: { id: 4242 }, text: '/settings' },
153
+ link,
154
+ });
155
+ const settingsMenu = menuEvents.at(-1);
156
+ const languageButton = findButton(
157
+ settingsMenu.extra.reply_markup,
158
+ (candidate) => /language|dil/i.test(candidate.text),
159
+ );
160
+ assert.ok(languageButton, 'settings menu should expose a language button');
161
+
162
+ menuEvents.length = 0;
163
+ await handleTelegramControlCallback({
164
+ bot: menuBot,
165
+ query: {
166
+ id: 'query-language-menu',
167
+ data: languageButton.callback_data,
168
+ message: { chat: { id: 4242 }, message_id: 77 },
169
+ },
170
+ link,
171
+ });
172
+ assert.equal(menuEvents.length, 1, 'callback menus should replace the existing menu message');
173
+ assert.equal(menuEvents[0].type, 'edit', 'callback menus should use editMessageText');
174
+ const trButton = findButton(menuEvents[0].extra.reply_markup, (candidate) => candidate.text === 'tr');
175
+ assert.ok(trButton, 'language menu should include Turkish');
176
+
177
+ menuEvents.length = 0;
178
+ await handleTelegramControlCallback({
179
+ bot: menuBot,
180
+ query: {
181
+ id: 'query-language-tr',
182
+ data: trButton.callback_data,
183
+ message: { chat: { id: 4242 }, message_id: 77 },
184
+ },
185
+ link,
186
+ });
187
+ assert.equal(telegramLinksDb.getByUserId(userId).language, 'tr', 'language selection should persist');
188
+ assert.equal(menuEvents.length, 1, 'language selection should not send a confirmation plus a second menu');
189
+ assert.equal(menuEvents[0].type, 'edit', 'language selection should replace the menu in place');
190
+ assert.ok(
191
+ menuEvents[0].text.includes('Pixcode Telegram kontrol merkezi'),
192
+ `Turkish menu should render after selection, got "${menuEvents[0].text}"`,
193
+ );
194
+ assert.ok(
195
+ !menuEvents[0].text.includes('Project:'),
196
+ 'Turkish menu should not keep English summary labels after language selection',
197
+ );
198
+
199
+ const botLevelMessages = [];
200
+ setTelegramBotForTesting({
201
+ sendMessage: async (chatId, text, extra = {}) => {
202
+ botLevelMessages.push({ chatId, text, extra });
203
+ return { ok: true };
204
+ },
205
+ answerCallbackQuery: async () => ({ ok: true }),
206
+ });
207
+
208
+ const expectBotReply = async (input, expectedFragment) => {
209
+ botLevelMessages.length = 0;
210
+ await handleIncomingTelegramMessage({
211
+ chat: { id: 'chat-4242' },
212
+ text: input,
213
+ message_id: 1,
214
+ from: { username: 'ali' },
215
+ });
216
+ assert.ok(botLevelMessages.length > 0, `${input} should reply from bot.js`);
217
+ assert.ok(
218
+ botLevelMessages[0].text.includes(expectedFragment),
219
+ `${input} should include "${expectedFragment}" but got "${botLevelMessages[0].text}"`,
220
+ );
221
+ assert.ok(
222
+ !botLevelMessages.some((entry) => entry.text.includes('Mesaj son oturumuna iletildi')),
223
+ `${input} should never hit the bridge queue reply`,
224
+ );
225
+ };
226
+
227
+ await expectBotReply('/start', 'Nasıl başlarsın');
228
+ await expectBotReply('/help', 'Komutlar:');
229
+
230
+ telegramLinksDb.unlink(userId);
231
+ } catch (error) {
232
+ failures.push(error?.message || String(error));
233
+ } finally {
234
+ rmSync(runtimeDir, { recursive: true, force: true });
235
+ }
236
+
237
+ if (failures.length > 0) {
238
+ console.error(`Telegram control smoke failed:\n- ${failures.join('\n- ')}`);
239
+ process.exit(1);
240
+ }
241
+
242
+ console.log('telegram control smoke passed');
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ const checks = [
6
+ {
7
+ name: 'update frequency defaults to 30 minutes',
8
+ file: 'src/utils/updateCheckPreferences.ts',
9
+ test: (source) => (
10
+ source.includes("export type UpdateCheckFrequency = 'off' | '30m' |")
11
+ && source.includes("{ value: '30m'")
12
+ && /DEFAULT_UPDATE_CHECK_PREFERENCES[\s\S]*frequency:\s*'30m'/.test(source)
13
+ ),
14
+ },
15
+ {
16
+ name: 'sidebar opens the version modal when an update is detected',
17
+ file: 'src/components/sidebar/view/Sidebar.tsx',
18
+ test: (source) => (
19
+ source.includes('PIXCODE_UPDATE_AVAILABLE_EVENT')
20
+ && source.includes('setShowVersionModal(true)')
21
+ && source.includes('updateAvailable')
22
+ ),
23
+ },
24
+ {
25
+ name: 'version modal supports release-notes-only opens',
26
+ file: 'src/components/version-upgrade/view/VersionUpgradeModal.tsx',
27
+ test: (source) => (
28
+ source.includes('isUpdateAvailable')
29
+ && source.includes('versionUpdate.releaseNotesTitle')
30
+ && source.includes('showUpdateActions')
31
+ ),
32
+ },
33
+ {
34
+ name: 'desktop splash makes startup update work visible',
35
+ file: 'desktop/electron/main.cjs',
36
+ test: (source) => (
37
+ source.includes('Checking for updates before launch')
38
+ && source.includes('Applying Pixcode update')
39
+ ),
40
+ },
41
+ ];
42
+
43
+ const failures = [];
44
+
45
+ for (const check of checks) {
46
+ const source = readFileSync(check.file, 'utf8');
47
+ if (!check.test(source)) failures.push(check.name);
48
+ }
49
+
50
+ if (failures.length > 0) {
51
+ console.error(`Update UX smoke failed:\n- ${failures.join('\n- ')}`);
52
+ process.exit(1);
53
+ }
54
+
55
+ console.log('update UX smoke passed');
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const source = readFileSync('src/components/sidebar/view/Sidebar.tsx', 'utf8');
7
+
8
+ assert.ok(
9
+ source.includes('VERSION_RELEASE_NOTES_SEEN_KEY'),
10
+ 'Sidebar should persist the latest equal-version release notes it auto-showed.',
11
+ );
12
+ assert.ok(
13
+ source.includes('localStorage.getItem(VERSION_RELEASE_NOTES_SEEN_KEY)'),
14
+ 'Sidebar should read the seen release-notes version from localStorage.',
15
+ );
16
+ assert.ok(
17
+ source.includes('localStorage.setItem(VERSION_RELEASE_NOTES_SEEN_KEY, latestVersion)'),
18
+ 'Sidebar should mark equal-version release notes as seen when auto-showing them.',
19
+ );
20
+ assert.ok(
21
+ source.includes('hasSeenCurrentReleaseNotes'),
22
+ 'Sidebar should avoid auto-showing release notes when the current version was already seen.',
23
+ );
24
+ assert.ok(
25
+ source.includes('!hasSeenCurrentReleaseNotes'),
26
+ 'The auto-show condition should be gated by the seen-version check.',
27
+ );
28
+
29
+ console.log('version modal autoshow smoke passed');
@@ -95,15 +95,25 @@ function quoteSystemdArg(arg) {
95
95
  return `"${String(arg).replace(/(["\\$`])/g, '\\$1')}"`;
96
96
  }
97
97
 
98
- function resolveDaemonCliEntryPath(context = {}) {
98
+ export function resolveDaemonCliEntryPath(context = {}) {
99
99
  const appRoot = context.appRoot || process.cwd();
100
100
  const explicitCliEntry = context.cliEntry ? path.resolve(context.cliEntry) : null;
101
101
  const argvCliEntry = process.argv[1] ? path.resolve(process.argv[1]) : null;
102
+ const distCliEntry = path.join(appRoot, 'dist-server', 'server', 'cli.js');
103
+ const sourceCliEntry = path.join(appRoot, 'server', 'cli.js');
104
+ const normalizeCliCandidate = (candidate) => {
105
+ if (!candidate) return null;
106
+ const resolved = path.resolve(candidate);
107
+ if (resolved === sourceCliEntry && fs.existsSync(distCliEntry)) {
108
+ return distCliEntry;
109
+ }
110
+ return resolved;
111
+ };
102
112
  const candidatePaths = [
103
- explicitCliEntry,
104
- argvCliEntry,
105
- path.join(appRoot, 'dist-server', 'server', 'cli.js'),
106
- path.join(appRoot, 'server', 'cli.js'),
113
+ normalizeCliCandidate(explicitCliEntry),
114
+ normalizeCliCandidate(argvCliEntry),
115
+ distCliEntry,
116
+ sourceCliEntry,
107
117
  ].filter(Boolean);
108
118
 
109
119
  const existingPath = candidatePaths.find(candidate => fs.existsSync(candidate));
@@ -111,7 +121,7 @@ function resolveDaemonCliEntryPath(context = {}) {
111
121
  return existingPath;
112
122
  }
113
123
 
114
- return explicitCliEntry || argvCliEntry || path.join(appRoot, 'server', 'cli.js');
124
+ return normalizeCliCandidate(explicitCliEntry) || normalizeCliCandidate(argvCliEntry) || distCliEntry;
115
125
  }
116
126
 
117
127
  function hasPixcodeBinary() {
@@ -210,12 +220,7 @@ function parseDaemonArgs(args) {
210
220
 
211
221
  function buildDaemonExecStart({ appRoot, serverPort, databasePath, nodeExecPath, cliEntry }) {
212
222
  const nodeExec = nodeExecPath || process.execPath || 'node';
213
- const cliCandidate = cliEntry
214
- ? path.resolve(cliEntry)
215
- : (process.argv[1] ? path.resolve(process.argv[1]) : path.join(appRoot, 'dist-server', 'server', 'cli.js'));
216
- const resolvedCliEntry = fs.existsSync(cliCandidate)
217
- ? cliCandidate
218
- : path.join(appRoot, 'dist-server', 'server', 'cli.js');
223
+ const resolvedCliEntry = resolveDaemonCliEntryPath({ appRoot, cliEntry });
219
224
 
220
225
  const args = [nodeExec, resolvedCliEntry, 'start', '--port', String(serverPort)];
221
226
  if (databasePath) {
@@ -217,6 +217,7 @@ function migrateSqliteIfPresent() {
217
217
  verified_at: tl.verified_at || null,
218
218
  notifications_enabled: tl.notifications_enabled !== 0,
219
219
  bridge_enabled: tl.bridge_enabled !== 0,
220
+ telegram_control: null,
220
221
  updated_at: tl.updated_at || nowIso(),
221
222
  });
222
223
  }
@@ -672,6 +673,40 @@ const appConfigDb = {
672
673
  // ---------------------------------------------------------------------------
673
674
  // Telegram — singleton config + per-user links
674
675
  // ---------------------------------------------------------------------------
676
+ const DEFAULT_TELEGRAM_CONTROL_STATE = {
677
+ remoteControlEnabled: true,
678
+ progressMode: 'final',
679
+ selectedProjectName: null,
680
+ selectedProjectPath: null,
681
+ selectedProvider: 'opencode',
682
+ selectedModel: null,
683
+ selectedWorkflowId: null,
684
+ awaiting: null,
685
+ };
686
+
687
+ function normalizeTelegramControlState(value = {}) {
688
+ const raw = value && typeof value === 'object' ? value : {};
689
+ const selectedProvider = ['claude', 'cursor', 'codex', 'gemini', 'qwen', 'opencode'].includes(raw.selectedProvider)
690
+ ? raw.selectedProvider
691
+ : DEFAULT_TELEGRAM_CONTROL_STATE.selectedProvider;
692
+ const progressMode = ['final', 'steps', 'all'].includes(raw.progressMode)
693
+ ? raw.progressMode
694
+ : DEFAULT_TELEGRAM_CONTROL_STATE.progressMode;
695
+
696
+ return {
697
+ ...DEFAULT_TELEGRAM_CONTROL_STATE,
698
+ ...raw,
699
+ remoteControlEnabled: raw.remoteControlEnabled !== false,
700
+ progressMode,
701
+ selectedProvider,
702
+ selectedProjectName: typeof raw.selectedProjectName === 'string' ? raw.selectedProjectName : null,
703
+ selectedProjectPath: typeof raw.selectedProjectPath === 'string' ? raw.selectedProjectPath : null,
704
+ selectedModel: typeof raw.selectedModel === 'string' ? raw.selectedModel : null,
705
+ selectedWorkflowId: typeof raw.selectedWorkflowId === 'string' ? raw.selectedWorkflowId : null,
706
+ awaiting: raw.awaiting && typeof raw.awaiting === 'object' ? raw.awaiting : null,
707
+ };
708
+ }
709
+
675
710
  const telegramConfigDb = {
676
711
  get: () => {
677
712
  const row = store.raw.telegram_config[0];
@@ -705,6 +740,7 @@ const telegramLinksDb = {
705
740
  verified_at: null,
706
741
  notifications_enabled: true,
707
742
  bridge_enabled: true,
743
+ telegram_control: normalizeTelegramControlState(),
708
744
  updated_at: nowIso(),
709
745
  });
710
746
  },
@@ -742,6 +778,7 @@ const telegramLinksDb = {
742
778
  language: row.language,
743
779
  notifications_enabled: row.notifications_enabled,
744
780
  bridge_enabled: row.bridge_enabled,
781
+ telegram_control: normalizeTelegramControlState(row.telegram_control),
745
782
  };
746
783
  },
747
784
  listVerified: () =>
@@ -752,6 +789,7 @@ const telegramLinksDb = {
752
789
  language: r.language,
753
790
  notifications_enabled: r.notifications_enabled,
754
791
  bridge_enabled: r.bridge_enabled,
792
+ telegram_control: normalizeTelegramControlState(r.telegram_control),
755
793
  })),
756
794
  updatePreferences: (userId, { language, notificationsEnabled, bridgeEnabled }) => {
757
795
  const patch = { updated_at: nowIso() };
@@ -761,6 +799,20 @@ const telegramLinksDb = {
761
799
  if (Object.keys(patch).length === 1) return; // only updated_at → no real change
762
800
  store.updateWhere('telegram_links', (r) => r.user_id === userId, patch);
763
801
  },
802
+ getControlState: (userId) => {
803
+ const row = store.findWhere('telegram_links', (r) => r.user_id === userId);
804
+ return normalizeTelegramControlState(row?.telegram_control);
805
+ },
806
+ updateControlState: (userId, patch) => {
807
+ const row = store.findWhere('telegram_links', (r) => r.user_id === userId);
808
+ const current = normalizeTelegramControlState(row?.telegram_control);
809
+ const next = normalizeTelegramControlState({ ...current, ...(patch || {}) });
810
+ store.updateWhere('telegram_links', (r) => r.user_id === userId, {
811
+ telegram_control: next,
812
+ updated_at: nowIso(),
813
+ });
814
+ return next;
815
+ },
764
816
  unlink: (userId) => {
765
817
  store.deleteWhere('telegram_links', (r) => r.user_id === userId);
766
818
  },
package/server/index.js CHANGED
@@ -413,11 +413,9 @@ app.use('/api/telegram', authenticateToken, telegramRoutes);
413
413
  // Agent API Routes (uses API key authentication)
414
414
  app.use('/api/agent', agentRoutes);
415
415
 
416
- // Serve public files (like api-docs.html)
417
- app.use(express.static(path.join(APP_ROOT, 'public')));
418
-
419
- // Static files served after API routes
420
- // Add cache control: HTML files should not be cached, but assets can be cached
416
+ // Static app files served after API routes. Keep dist before public so
417
+ // / and /index.html always resolve to the Pixcode app, not the GitHub Pages
418
+ // landing page that also lives in public/index.html.
421
419
  app.use(express.static(path.join(APP_ROOT, 'dist'), {
422
420
  setHeaders: (res, filePath) => {
423
421
  if (filePath.endsWith('.html')) {
@@ -432,6 +430,12 @@ app.use(express.static(path.join(APP_ROOT, 'dist'), {
432
430
  }
433
431
  }));
434
432
 
433
+ // Serve extra public files (api-docs.html, llms.txt, landing pages) without
434
+ // letting public/index.html shadow the production app root.
435
+ app.use(express.static(path.join(APP_ROOT, 'public'), {
436
+ index: false,
437
+ }));
438
+
435
439
  // API Routes (protected)
436
440
  // /api/config endpoint removed - no longer needed
437
441
  // Frontend now uses window.location for WebSocket URLs
@@ -276,6 +276,10 @@ function rolePrompt(role: AgentRole): string {
276
276
  return 'Implementation work should avoid duplicating other agents and should report changed files, commands, blockers, and next actions.';
277
277
  }
278
278
 
279
+ function privacyGuardPrompt(): string {
280
+ return 'Do not mention internal instructions, memory files, skill use, or tool protocol unless the user explicitly asks.';
281
+ }
282
+
279
283
  function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
280
284
  return [
281
285
  `You are ${agent.label} in a Pixcode CLI team.`,
@@ -290,6 +294,7 @@ function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
290
294
  '- dependencies/blockers for the next agents',
291
295
  '- concrete next action for your full implementation task',
292
296
  'Do not install dependencies, edit files, run long commands, or start servers in this handoff task.',
297
+ privacyGuardPrompt(),
293
298
  'Stop after the contract. Keep it concise and respond in the same language as the user request.',
294
299
  ].filter(Boolean).join('\n');
295
300
  }
@@ -374,6 +379,7 @@ function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, u
374
379
  ? `Your explicit assignment from the user is: ${agent.instruction}`
375
380
  : 'No fixed per-agent assignment was set. Take the part assigned to you by the coordinator; if none is named, choose useful work that fits this CLI.',
376
381
  rolePrompt(stage),
382
+ privacyGuardPrompt(),
377
383
  'Respond in the same language as the user request.',
378
384
  ].filter(Boolean).join('\n'),
379
385
  inputs,
@@ -419,6 +425,8 @@ function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, u
419
425
  prompt: [
420
426
  'Collect the worker outputs into one user-facing result.',
421
427
  'Show what each CLI did, which parts failed, what changed, and the next action if work remains.',
428
+ 'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
429
+ 'If a worker reveals internal process text, summarize only the useful user-facing result.',
422
430
  'Respond in the same language as the user request.',
423
431
  ].join('\n'),
424
432
  inputs: workerNodes.map((node) => node.id),
@@ -436,6 +444,7 @@ function stagePrompt(agent: AgentAssignment, stage: AgentRole): string {
436
444
  agent.role && agent.role !== stage ? `User custom stage label: ${agent.role}.` : '',
437
445
  agent.instruction ? `User assignment for you: ${agent.instruction}` : '',
438
446
  rolePrompt(stage),
447
+ privacyGuardPrompt(),
439
448
  'Keep the answer concise, structured, and useful for the next stage.',
440
449
  'Respond in the same language as the user request.',
441
450
  ].filter(Boolean).join('\n');
@@ -605,6 +614,7 @@ function expandSequentialHandoffWorkflow(workflow: Workflow, metadata?: Record<s
605
614
  ? `Your explicit assignment from the user is: ${agent.instruction}`
606
615
  : 'Use the prior step output and do the next most useful handoff step for the user goal.',
607
616
  'Report changed files, commands, blockers, and the next handoff requirement.',
617
+ privacyGuardPrompt(),
608
618
  'Respond in the same language as the user request.',
609
619
  ].filter(Boolean).join('\n'),
610
620
  inputs: index === 0 ? [] : [safeAgentNodeId(agents[index - 1], index - 1, 'handoff')],
@@ -646,6 +656,7 @@ function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unkn
646
656
  `You are ${agent.label}.`,
647
657
  'Review the requested change for bugs, regressions, missing validation, security, scale, and user-experience risks.',
648
658
  agent.instruction ? `Focus on this user assignment: ${agent.instruction}` : '',
659
+ privacyGuardPrompt(),
649
660
  'Respond in the same language as the user request.',
650
661
  ].filter(Boolean).join('\n'),
651
662
  inputs: [],
@@ -666,7 +677,11 @@ function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unkn
666
677
  model: reportAgent.model,
667
678
  permissionMode: reportAgent.permissionMode,
668
679
  toolsSettings: reportAgent.toolsSettings,
669
- prompt: 'Aggregate the prior agent reviews into a concise prioritized report. Respond in the same language as the user request.',
680
+ prompt: [
681
+ 'Aggregate the prior agent reviews into a concise prioritized report.',
682
+ 'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
683
+ 'Respond in the same language as the user request.',
684
+ ].join('\n'),
670
685
  inputs: reviewNodes.map((node) => node.id),
671
686
  output: 'message',
672
687
  onFail: 'abort',
@@ -701,13 +716,13 @@ function readTaskResult(task: RawTask): TaskResult {
701
716
  };
702
717
  });
703
718
  const outputMessages = messages.filter((message) => message.role !== 'user');
704
- const text = outputMessages.map((message) => `${message.role}: ${message.text}`).join('\n\n');
719
+ const userFacingTaskText = outputMessages.map((message) => message.text.trim()).filter(Boolean).join('\n\n');
705
720
  const error = task.error?.message
706
721
  ? `${task.error.code ? `${task.error.code}: ` : ''}${task.error.message}`
707
722
  : undefined;
708
723
  return {
709
724
  state: task.state ?? 'submitted',
710
- text,
725
+ text: userFacingTaskText,
711
726
  error,
712
727
  messages,
713
728
  artifacts,
@@ -33,6 +33,7 @@ router.get('/status', (req, res) => {
33
33
  language: link.language,
34
34
  notificationsEnabled: Boolean(link.notifications_enabled),
35
35
  bridgeEnabled: Boolean(link.bridge_enabled),
36
+ control: telegramLinksDb.getControlState(req.user.id),
36
37
  pairingCode: link.pairing_code,
37
38
  pairingExpiresAt: link.pairing_code_expires_at,
38
39
  verifiedAt: link.verified_at,
@@ -98,13 +99,27 @@ router.post('/pairing-code', (req, res) => {
98
99
  // PATCH /api/telegram/link — update language / toggles on the user's link
99
100
  router.patch('/link', (req, res) => {
100
101
  try {
101
- const { language, notificationsEnabled, bridgeEnabled } = req.body || {};
102
+ const { language, notificationsEnabled, bridgeEnabled, controlEnabled, progressMode } = req.body || {};
102
103
  const payload = {};
103
104
  if (language !== undefined) payload.language = sanitizeLanguage(language);
104
105
  if (notificationsEnabled !== undefined) payload.notificationsEnabled = Boolean(notificationsEnabled);
105
106
  if (bridgeEnabled !== undefined) payload.bridgeEnabled = Boolean(bridgeEnabled);
106
107
  telegramLinksDb.updatePreferences(req.user.id, payload);
107
- res.json({ success: true, link: telegramLinksDb.getByUserId(req.user.id) });
108
+
109
+ const controlPatch = {};
110
+ if (controlEnabled !== undefined) controlPatch.remoteControlEnabled = Boolean(controlEnabled);
111
+ if (progressMode !== undefined && ['final', 'steps', 'all'].includes(progressMode)) {
112
+ controlPatch.progressMode = progressMode;
113
+ }
114
+ if (Object.keys(controlPatch).length > 0) {
115
+ telegramLinksDb.updateControlState(req.user.id, controlPatch);
116
+ }
117
+
118
+ res.json({
119
+ success: true,
120
+ link: telegramLinksDb.getByUserId(req.user.id),
121
+ control: telegramLinksDb.getControlState(req.user.id),
122
+ });
108
123
  } catch (error) {
109
124
  console.error('telegram/link patch failed:', error);
110
125
  res.status(500).json({ error: 'Failed to update link' });