cli-jaw 2.17.61 → 2.17.62
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.ja.md +376 -339
- package/README.ko.md +338 -393
- package/README.md +163 -508
- package/README.zh-CN.md +365 -376
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -1
- package/dist/src/agent/pi-runtime.js +37 -18
- package/dist/src/agent/pi-runtime.js.map +1 -1
- package/dist/src/agent/runtime/acp/permissions.js +2 -3
- package/dist/src/agent/runtime/acp/permissions.js.map +1 -1
- package/dist/src/agent/runtime/pi-turn.js +7 -0
- package/dist/src/agent/runtime/pi-turn.js.map +1 -1
- package/dist/src/agent/spawn/line-buffer.js +84 -13
- package/dist/src/agent/spawn/line-buffer.js.map +1 -1
- package/dist/src/agent/spawn.js +26 -15
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/code-mode/host.js +34 -5
- package/dist/src/code-mode/host.js.map +1 -1
- package/dist/src/code-mode/manager.js +24 -7
- package/dist/src/code-mode/manager.js.map +1 -1
- package/dist/src/code-mode/providers/provider-live-models.js +3 -1
- package/dist/src/code-mode/providers/provider-live-models.js.map +1 -1
- package/dist/src/code-mode/store.js +31 -6
- package/dist/src/code-mode/store.js.map +1 -1
- package/dist/src/core/config.js +119 -17
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/runtime-settings.js +7 -1
- package/dist/src/core/runtime-settings.js.map +1 -1
- package/dist/src/core/settings-merge.js +9 -0
- package/dist/src/core/settings-merge.js.map +1 -1
- package/dist/src/core/settings-watch.js +19 -1
- package/dist/src/core/settings-watch.js.map +1 -1
- package/dist/src/discord/bot.js +6 -4
- package/dist/src/discord/bot.js.map +1 -1
- package/dist/src/discord/send-only-client.js +37 -3
- package/dist/src/discord/send-only-client.js.map +1 -1
- package/dist/src/manager/memory/embedding/sync.js +6 -0
- package/dist/src/manager/memory/embedding/sync.js.map +1 -1
- package/dist/src/manager/routes/dashboard-memory.js +20 -7
- package/dist/src/manager/routes/dashboard-memory.js.map +1 -1
- package/dist/src/manager/server.js +8 -0
- package/dist/src/manager/server.js.map +1 -1
- package/dist/src/manager/shutdown.js +29 -3
- package/dist/src/manager/shutdown.js.map +1 -1
- package/dist/src/manager/worker-events.js +70 -8
- package/dist/src/manager/worker-events.js.map +1 -1
- package/dist/src/routes/code-native.js +23 -3
- package/dist/src/routes/code-native.js.map +1 -1
- package/dist/src/routes/events.js +4 -0
- package/dist/src/routes/events.js.map +1 -1
- package/dist/src/routes/settings.js +45 -1
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/routes/sse-connections.js +31 -0
- package/dist/src/routes/sse-connections.js.map +1 -0
- package/dist/src/shared/permissions.js +47 -0
- package/dist/src/shared/permissions.js.map +1 -0
- package/package.json +1 -1
- package/public/dist/.vite/manifest.json +31 -31
- package/public/dist/assets/AdvancedExport-B7h_ZvBu.js +1 -0
- package/public/dist/assets/Agent-DnbZJP7b.js +1 -0
- package/public/dist/assets/{ChannelEnablementControl-B2tq8Ji4.js → ChannelEnablementControl-Dl0uYx38.js} +5 -5
- package/public/dist/assets/ChannelSetupEntry-DxtOrN01.js +1 -0
- package/public/dist/assets/ChannelsDiscord-BEGGwd_3.js +1 -0
- package/public/dist/assets/{ChannelsSlack-B9FV0ftr.js → ChannelsSlack-BryC2Ahr.js} +1 -1
- package/public/dist/assets/ChannelsTelegram-CGSpaImJ.js +1 -0
- package/public/dist/assets/CodeCanvas-DpkFi5Au.js +2 -0
- package/public/dist/assets/Permissions-BwiMEoTV.js +1 -0
- package/public/dist/assets/Permissions-CodIW-Yd.js +1 -0
- package/public/dist/assets/{manager-C5cU1joU.js → manager-Bi_xw7TW.js} +2 -2
- package/public/dist/assets/{settings-CA2LQDGh.js → settings-W4KG9ivu.js} +1 -1
- package/public/dist/assets/{settings-embedding-oGvFayh-.js → settings-embedding-BfWzWKMM.js} +1 -1
- package/public/dist/manager/index.html +1 -1
- package/public/dist/settings/index.html +1 -1
- package/public/js/features/settings-discord.ts +39 -5
- package/public/js/features/settings-telegram.ts +38 -4
- package/public/js/features/settings-types.ts +2 -0
- package/public/locales/en.json +2 -0
- package/public/locales/ja.json +2 -0
- package/public/locales/ko.json +2 -0
- package/public/locales/zh.json +2 -0
- package/public/manager/src/App.tsx +4 -1
- package/public/manager/src/code/code-controller-runtime.ts +13 -6
- package/public/manager/src/code/code-session-client.ts +1 -1
- package/public/manager/src/code/session-order.ts +6 -7
- package/public/manager/src/dashboard-url-state.ts +8 -0
- package/public/manager/src/settings/pages/AdvancedExport.tsx +2 -0
- package/public/manager/src/settings/pages/Agent.tsx +13 -5
- package/public/manager/src/settings/pages/ChannelsDiscord.tsx +27 -4
- package/public/manager/src/settings/pages/ChannelsTelegram.tsx +26 -4
- package/public/manager/src/settings/pages/Permissions.tsx +50 -27
- package/public/manager/src/settings/pages/components/ChannelSetupEntry.tsx +4 -3
- package/public/manager/src/settings/pages/components/agent/PermissionQuickSection.tsx +10 -2
- package/public/dist/assets/AdvancedExport-BNMjYkw6.js +0 -1
- package/public/dist/assets/Agent-D4g8TPjm.js +0 -1
- package/public/dist/assets/ChannelSetupEntry-zKYImWXx.js +0 -1
- package/public/dist/assets/ChannelsDiscord-CWRKV-Zl.js +0 -1
- package/public/dist/assets/ChannelsTelegram-BdK-fTPI.js +0 -1
- package/public/dist/assets/CodeCanvas-D5jp8-t5.js +0 -2
- package/public/dist/assets/Permissions-Bh947tUG.js +0 -1
- package/public/dist/assets/Permissions-DzNJCt8K.js +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link rel="icon" type="image/png" href="/dist/assets/icon-192-CADbZUeU.png">
|
|
7
7
|
<title>Jaw Manager</title>
|
|
8
8
|
<script src="/manager/theme-boot.js"></script>
|
|
9
|
-
<script type="module" crossorigin src="/dist/assets/manager-
|
|
9
|
+
<script type="module" crossorigin src="/dist/assets/manager-Bi_xw7TW.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-DSSkXCir.css">
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/dist/assets/settings-embedding-DhEdT86W.css">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/dist/assets/manager-BnQHAKk6.css">
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Settings</title>
|
|
7
7
|
<script src="/manager/theme-boot.js"></script>
|
|
8
|
-
<script type="module" crossorigin src="/dist/assets/settings-
|
|
8
|
+
<script type="module" crossorigin src="/dist/assets/settings-W4KG9ivu.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/dist/assets/settings-embedding-DhEdT86W.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
// ── Discord Settings ──
|
|
2
2
|
import { apiJson } from '../api.js';
|
|
3
3
|
import { openSetupGuideIfUnconfigured } from './channel-setup-guide.js';
|
|
4
|
+
import { t } from './i18n.js';
|
|
4
5
|
import type { SettingsData } from './settings-types.js';
|
|
5
6
|
|
|
7
|
+
let discordEnvironmentVariables: string[] = [];
|
|
8
|
+
|
|
9
|
+
function discordEnvironmentMessage(): string {
|
|
10
|
+
return t('settings.discord.managedByEnvironment', {
|
|
11
|
+
variables: discordEnvironmentVariables.join(', ') || 'DISCORD_*',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export async function saveDiscordSettings(): Promise<void> {
|
|
16
|
+
if (discordEnvironmentVariables.length > 0) {
|
|
17
|
+
window.alert(discordEnvironmentMessage());
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
7
20
|
const token = (document.getElementById('dcToken') as HTMLInputElement)?.value.trim() || '';
|
|
8
21
|
const guildId = (document.getElementById('dcGuildId') as HTMLInputElement)?.value.trim() || '';
|
|
9
22
|
const channelIdsRaw = (document.getElementById('dcChannelIds') as HTMLInputElement)?.value.trim() || '';
|
|
@@ -14,6 +27,10 @@ export async function saveDiscordSettings(): Promise<void> {
|
|
|
14
27
|
}
|
|
15
28
|
|
|
16
29
|
export async function setDiscord(enabled: boolean): Promise<void> {
|
|
30
|
+
if (discordEnvironmentVariables.length > 0) {
|
|
31
|
+
window.alert(discordEnvironmentMessage());
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
17
34
|
document.getElementById('dcOn')?.classList.toggle('active', enabled);
|
|
18
35
|
document.getElementById('dcOff')?.classList.toggle('active', !enabled);
|
|
19
36
|
await apiJson('/api/settings', 'PUT', { discord: { enabled } });
|
|
@@ -41,16 +58,33 @@ export async function setDiscordMentionOnly(enabled: boolean): Promise<void> {
|
|
|
41
58
|
export function loadDiscordSettings(s: SettingsData): void {
|
|
42
59
|
if (!s.discord) return;
|
|
43
60
|
const dc = s.discord;
|
|
61
|
+
discordEnvironmentVariables = Array.isArray(s.discordEnvironmentVariables)
|
|
62
|
+
? s.discordEnvironmentVariables
|
|
63
|
+
: [];
|
|
64
|
+
const environmentManaged = discordEnvironmentVariables.length > 0;
|
|
65
|
+
const notice = document.getElementById('discord-environment-managed');
|
|
66
|
+
if (notice) {
|
|
67
|
+
notice.style.display = environmentManaged ? '' : 'none';
|
|
68
|
+
notice.textContent = environmentManaged
|
|
69
|
+
? t('settings.discord.managedByEnvironment', { variables: discordEnvironmentVariables.join(', ') })
|
|
70
|
+
: '';
|
|
71
|
+
}
|
|
72
|
+
for (const id of ['dcToken', 'dcGuildId', 'dcChannelIds']) {
|
|
73
|
+
const input = document.getElementById(id) as HTMLInputElement | null;
|
|
74
|
+
if (input) input.disabled = environmentManaged;
|
|
75
|
+
}
|
|
76
|
+
for (const id of ['dcOff', 'dcOn', 'discord-onboarding-trigger']) {
|
|
77
|
+
const button = document.getElementById(id) as HTMLButtonElement | null;
|
|
78
|
+
if (button) button.disabled = environmentManaged;
|
|
79
|
+
}
|
|
44
80
|
document.getElementById('dcOn')?.classList.toggle('active', !!dc.enabled);
|
|
45
81
|
document.getElementById('dcOff')?.classList.toggle('active', !dc.enabled);
|
|
46
82
|
const dcToken = document.getElementById('dcToken') as HTMLInputElement | null;
|
|
47
|
-
if (
|
|
83
|
+
if (dcToken) dcToken.value = environmentManaged ? '' : dc.token || '';
|
|
48
84
|
const dcGuildId = document.getElementById('dcGuildId') as HTMLInputElement | null;
|
|
49
|
-
if (
|
|
85
|
+
if (dcGuildId) dcGuildId.value = environmentManaged ? '' : dc.guildId || '';
|
|
50
86
|
const dcChannelIds = document.getElementById('dcChannelIds') as HTMLInputElement | null;
|
|
51
|
-
if (dc.channelIds?.
|
|
52
|
-
dcChannelIds.value = dc.channelIds.join(', ');
|
|
53
|
-
}
|
|
87
|
+
if (dcChannelIds) dcChannelIds.value = environmentManaged ? '' : dc.channelIds?.join(', ') || '';
|
|
54
88
|
const fwdOn = dc.forwardAll !== false;
|
|
55
89
|
document.getElementById('dcForwardOn')?.classList.toggle('active', fwdOn);
|
|
56
90
|
document.getElementById('dcForwardOff')?.classList.toggle('active', !fwdOn);
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
// ── Telegram Settings ──
|
|
2
2
|
import { apiJson } from '../api.js';
|
|
3
3
|
import { openSetupGuideIfUnconfigured } from './channel-setup-guide.js';
|
|
4
|
+
import { t } from './i18n.js';
|
|
4
5
|
import type { SettingsData } from './settings-types.js';
|
|
5
6
|
|
|
7
|
+
let telegramEnvironmentVariables: string[] = [];
|
|
8
|
+
|
|
9
|
+
function telegramEnvironmentMessage(): string {
|
|
10
|
+
return t('settings.telegram.managedByEnvironment', {
|
|
11
|
+
variables: telegramEnvironmentVariables.join(', ') || 'TELEGRAM_*',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export async function saveTelegramSettings(): Promise<void> {
|
|
16
|
+
if (telegramEnvironmentVariables.length > 0) {
|
|
17
|
+
window.alert(telegramEnvironmentMessage());
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
7
20
|
const token = (document.getElementById('tgToken') as HTMLInputElement)?.value.trim() || '';
|
|
8
21
|
const chatIdsRaw = (document.getElementById('tgChatIds') as HTMLInputElement)?.value.trim() || '';
|
|
9
22
|
const allowedChatIds = chatIdsRaw
|
|
@@ -13,6 +26,10 @@ export async function saveTelegramSettings(): Promise<void> {
|
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
export async function setTelegram(enabled: boolean): Promise<void> {
|
|
29
|
+
if (telegramEnvironmentVariables.length > 0) {
|
|
30
|
+
window.alert(telegramEnvironmentMessage());
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
16
33
|
document.getElementById('tgOn')?.classList.toggle('active', enabled);
|
|
17
34
|
document.getElementById('tgOff')?.classList.toggle('active', !enabled);
|
|
18
35
|
await apiJson('/api/settings', 'PUT', { telegram: { enabled } });
|
|
@@ -34,14 +51,31 @@ export async function setTelegramMentionOnly(enabled: boolean): Promise<void> {
|
|
|
34
51
|
export function loadTelegramSettings(s: SettingsData): void {
|
|
35
52
|
if (!s.telegram) return;
|
|
36
53
|
const tg = s.telegram;
|
|
54
|
+
telegramEnvironmentVariables = Array.isArray(s.telegramEnvironmentVariables)
|
|
55
|
+
? s.telegramEnvironmentVariables
|
|
56
|
+
: [];
|
|
57
|
+
const environmentManaged = telegramEnvironmentVariables.length > 0;
|
|
58
|
+
const notice = document.getElementById('telegram-environment-managed');
|
|
59
|
+
if (notice) {
|
|
60
|
+
notice.style.display = environmentManaged ? '' : 'none';
|
|
61
|
+
notice.textContent = environmentManaged
|
|
62
|
+
? t('settings.telegram.managedByEnvironment', { variables: telegramEnvironmentVariables.join(', ') })
|
|
63
|
+
: '';
|
|
64
|
+
}
|
|
65
|
+
for (const id of ['tgToken', 'tgChatIds']) {
|
|
66
|
+
const input = document.getElementById(id) as HTMLInputElement | null;
|
|
67
|
+
if (input) input.disabled = environmentManaged;
|
|
68
|
+
}
|
|
69
|
+
for (const id of ['tgOff', 'tgOn', 'telegram-onboarding-trigger']) {
|
|
70
|
+
const button = document.getElementById(id) as HTMLButtonElement | null;
|
|
71
|
+
if (button) button.disabled = environmentManaged;
|
|
72
|
+
}
|
|
37
73
|
document.getElementById('tgOn')?.classList.toggle('active', !!tg.enabled);
|
|
38
74
|
document.getElementById('tgOff')?.classList.toggle('active', !tg.enabled);
|
|
39
75
|
const tgToken = document.getElementById('tgToken') as HTMLInputElement | null;
|
|
40
|
-
if (
|
|
76
|
+
if (tgToken) tgToken.value = environmentManaged ? '' : tg.token || '';
|
|
41
77
|
const tgChatIds = document.getElementById('tgChatIds') as HTMLInputElement | null;
|
|
42
|
-
if (tg.allowedChatIds?.
|
|
43
|
-
tgChatIds.value = tg.allowedChatIds.join(', ');
|
|
44
|
-
}
|
|
78
|
+
if (tgChatIds) tgChatIds.value = environmentManaged ? '' : tg.allowedChatIds?.join(', ') || '';
|
|
45
79
|
const fwdOn = tg.forwardAll !== false;
|
|
46
80
|
document.getElementById('tgForwardOn')?.classList.toggle('active', fwdOn);
|
|
47
81
|
document.getElementById('tgForwardOff')?.classList.toggle('active', !fwdOn);
|
|
@@ -153,6 +153,8 @@ export interface SettingsData {
|
|
|
153
153
|
discord?: DiscordConfig;
|
|
154
154
|
slack?: SlackConfig;
|
|
155
155
|
slackEnvironmentVariables?: string[];
|
|
156
|
+
telegramEnvironmentVariables?: string[];
|
|
157
|
+
discordEnvironmentVariables?: string[];
|
|
156
158
|
/** @deprecated Use messaging.homeChannel (response alias). */
|
|
157
159
|
channel?: 'telegram' | 'discord' | 'slack';
|
|
158
160
|
messaging?: MessagingConfig;
|
package/public/locales/en.json
CHANGED
|
@@ -873,6 +873,8 @@
|
|
|
873
873
|
"settings.slack.resetFailed": "Could not reset the Slack app connection. Try again.",
|
|
874
874
|
"settings.slack.resetManagedByEnvironment": "This Slack connection is managed by environment variables ({variables}). Remove them and restart cli-jaw before resetting the connection.",
|
|
875
875
|
"settings.slack.managedByEnvironment": "Connection settings are read-only because they are managed by environment variables: {variables}. Remove them and restart cli-jaw to edit here.",
|
|
876
|
+
"settings.telegram.managedByEnvironment": "Connection settings are read-only because they are managed by environment variables: {variables}. Remove them and restart cli-jaw to edit here.",
|
|
877
|
+
"settings.discord.managedByEnvironment": "Connection settings are read-only because they are managed by environment variables: {variables}. Remove them and restart cli-jaw to edit here.",
|
|
876
878
|
"settings.slack.attachPort": "Attach instance port",
|
|
877
879
|
"settings.channel.notAttachInstanceHint": "Another instance owns this Slack connection (check the attach instance port)",
|
|
878
880
|
"settings.slack.guide.step2Desc": "App settings → OAuth & Permissions → confirm files:read → Reinstall to Workspace → copy Bot User OAuth Token",
|
package/public/locales/ja.json
CHANGED
|
@@ -862,6 +862,8 @@
|
|
|
862
862
|
"settings.slack.resetFailed": "Slack アプリの接続をリセットできませんでした。もう一度お試しください。",
|
|
863
863
|
"settings.slack.resetManagedByEnvironment": "この Slack 接続は環境変数({variables})で管理されています。環境変数を削除して cli-jaw を再起動してから、接続をリセットしてください。",
|
|
864
864
|
"settings.slack.managedByEnvironment": "接続設定は環境変数({variables})で管理されているため読み取り専用です。ここで編集するには環境変数を削除し、cli-jaw を再起動してください。",
|
|
865
|
+
"settings.telegram.managedByEnvironment": "接続設定は環境変数({variables})で管理されているため読み取り専用です。ここで編集するには環境変数を削除し、cli-jaw を再起動してください。",
|
|
866
|
+
"settings.discord.managedByEnvironment": "接続設定は環境変数({variables})で管理されているため読み取り専用です。ここで編集するには環境変数を削除し、cli-jaw を再起動してください。",
|
|
865
867
|
"settings.slack.attachPort": "接続インスタンスのポート",
|
|
866
868
|
"settings.channel.notAttachInstanceHint": "別のインスタンスがこの Slack 接続を所有しています(接続インスタンスのポートを確認)",
|
|
867
869
|
"settings.slack.guide.step2Desc": "アプリ設定 → OAuth & Permissions → files:read を確認 → Reinstall to Workspace → Bot User OAuth Token をコピー",
|
package/public/locales/ko.json
CHANGED
|
@@ -873,6 +873,8 @@
|
|
|
873
873
|
"settings.slack.resetFailed": "Slack 앱 연결을 초기화하지 못했습니다. 다시 시도해주세요.",
|
|
874
874
|
"settings.slack.resetManagedByEnvironment": "이 Slack 연결은 환경변수({variables})로 관리되고 있습니다. 해당 환경변수를 제거하고 cli-jaw를 재시작한 후 연결을 초기화하세요.",
|
|
875
875
|
"settings.slack.managedByEnvironment": "연결 설정은 환경변수({variables})로 관리되어 읽기 전용입니다. 여기서 수정하려면 환경변수를 제거하고 cli-jaw를 재시작하세요.",
|
|
876
|
+
"settings.telegram.managedByEnvironment": "연결 설정은 환경변수({variables})로 관리되어 읽기 전용입니다. 여기서 수정하려면 환경변수를 제거하고 cli-jaw를 재시작하세요.",
|
|
877
|
+
"settings.discord.managedByEnvironment": "연결 설정은 환경변수({variables})로 관리되어 읽기 전용입니다. 여기서 수정하려면 환경변수를 제거하고 cli-jaw를 재시작하세요.",
|
|
876
878
|
"settings.slack.attachPort": "연결 인스턴스 포트",
|
|
877
879
|
"settings.channel.notAttachInstanceHint": "다른 인스턴스가 이 Slack 연결을 소유하고 있습니다 (연결 인스턴스 포트 확인)",
|
|
878
880
|
"settings.slack.guide.step2Desc": "앱 설정 → OAuth & Permissions → files:read 확인 → Reinstall to Workspace → Bot User OAuth Token 복사",
|
package/public/locales/zh.json
CHANGED
|
@@ -862,6 +862,8 @@
|
|
|
862
862
|
"settings.slack.resetFailed": "无法重置 Slack 应用连接,请重试。",
|
|
863
863
|
"settings.slack.resetManagedByEnvironment": "此 Slack 连接由环境变量({variables})管理。请删除这些环境变量并重启 cli-jaw,然后再重置连接。",
|
|
864
864
|
"settings.slack.managedByEnvironment": "连接设置由环境变量({variables})管理,因此为只读。若要在此编辑,请删除这些变量并重启 cli-jaw。",
|
|
865
|
+
"settings.telegram.managedByEnvironment": "连接设置由环境变量({variables})管理,因此为只读。若要在此编辑,请删除这些变量并重启 cli-jaw。",
|
|
866
|
+
"settings.discord.managedByEnvironment": "连接设置由环境变量({variables})管理,因此为只读。若要在此编辑,请删除这些变量并重启 cli-jaw。",
|
|
865
867
|
"settings.slack.attachPort": "连接实例端口",
|
|
866
868
|
"settings.channel.notAttachInstanceHint": "另一个实例拥有此 Slack 连接(请检查连接实例端口)",
|
|
867
869
|
"settings.slack.guide.step2Desc": "应用设置 → OAuth & Permissions → 确认 files:read → Reinstall to Workspace → 复制 Bot User OAuth Token",
|
|
@@ -19,7 +19,7 @@ import { useInvalidationSubscription } from './sync/useInvalidationSubscription'
|
|
|
19
19
|
import type { BoardView } from './dashboard-board/board-view';
|
|
20
20
|
import { type ScheduleGroup } from './dashboard-schedule/DashboardScheduleSidebar';
|
|
21
21
|
import { normalizeSidebarModeForBuild, REMINDERS_WORKSPACE_ENABLED } from './dashboard-features';
|
|
22
|
-
import { readInitialSidebarMode, readTrayRemindersMode } from './dashboard-url-state';
|
|
22
|
+
import { readInitialSelectedPort, readInitialSidebarMode, readTrayRemindersMode } from './dashboard-url-state';
|
|
23
23
|
import { TrayRemindersApp } from './dashboard-reminders/TrayRemindersApp';
|
|
24
24
|
import { useDashboardRegistry } from './hooks/useDashboardRegistry';
|
|
25
25
|
import { useDashboardView, hydrateInstanceSettings, createInstanceSettingsNavigation, useSettingsDirtyState } from './hooks/useDashboardView';
|
|
@@ -195,6 +195,9 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
|
|
|
195
195
|
const ui = loaded.registry.ui;
|
|
196
196
|
const restored = hydrateInstanceSettings(ui);
|
|
197
197
|
view.setSelectedPort(ui.selectedPort); view.setActiveDetailTab(restored.selectedTab); view.setSidebarCollapsed(ui.sidebarCollapsed);
|
|
198
|
+
// Menu bar "open instance" links win over the restored selection without persisting it.
|
|
199
|
+
const urlPort = readInitialSelectedPort(window.location.search);
|
|
200
|
+
if (urlPort !== null) view.setSelectedPort(urlPort);
|
|
198
201
|
view.setActivityDockCollapsed(ui.activityDockCollapsed); view.setActivityDockHeight(ui.activityDockHeight);
|
|
199
202
|
const sidebarMode = normalizeSidebarModeForBuild(readInitialSidebarMode(window.location.search) ?? restored.sidebarMode); // restored carries the legacy settings request
|
|
200
203
|
view.setSidebarMode(sidebarMode);
|
|
@@ -99,7 +99,8 @@ export class CodeController {
|
|
|
99
99
|
private historyLoading = new Set<string>();
|
|
100
100
|
private indexLoading = false;
|
|
101
101
|
private moreSessions = false;
|
|
102
|
-
private
|
|
102
|
+
private cursor: string | null = null;
|
|
103
|
+
private fetched = 0;
|
|
103
104
|
private indexError: string | null = null;
|
|
104
105
|
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
105
106
|
private listeners = new Set<() => void>();
|
|
@@ -153,6 +154,8 @@ export class CodeController {
|
|
|
153
154
|
this.summaries.clear();
|
|
154
155
|
this.attention.clear();
|
|
155
156
|
this.rows = [];
|
|
157
|
+
this.cursor = null;
|
|
158
|
+
this.fetched = 0;
|
|
156
159
|
this.gitGeneration++;
|
|
157
160
|
this.pickerGeneration++;
|
|
158
161
|
};
|
|
@@ -338,12 +341,15 @@ export class CodeController {
|
|
|
338
341
|
private async readIndex(more = false): Promise<void> {
|
|
339
342
|
const generation = ++this.indexGeneration;
|
|
340
343
|
const life = this.lifetime;
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
// Pages are keyset slices of the stable creation order; a refresh
|
|
345
|
+
// re-reads a grown prefix instead of resuming the stream.
|
|
346
|
+
const cursor = more ? this.cursor : null;
|
|
347
|
+
const limit = more ? INDEX_PAGE_SIZE : Math.min(MAX_INDEX, Math.max(INDEX_PAGE_SIZE, this.fetched));
|
|
343
348
|
this.indexLoading = true;
|
|
344
349
|
this.notify();
|
|
345
350
|
try {
|
|
346
|
-
const page = await this.client.listSessions({ ...this.filter, limit,
|
|
351
|
+
const page = await this.client.listSessions({ ...this.filter, limit,
|
|
352
|
+
...(cursor === null ? {} : { cursor }),
|
|
347
353
|
...(this.filter.scope === 'cwd' ? { cwd: this.workingDir } : {}) }, this.abort.signal);
|
|
348
354
|
if (!this.active || life !== this.lifetime || generation !== this.indexGeneration) return;
|
|
349
355
|
for (const session of page.sessions) {
|
|
@@ -357,7 +363,8 @@ export class CodeController {
|
|
|
357
363
|
}
|
|
358
364
|
}
|
|
359
365
|
this.rows = [...new Set([...(more ? this.rows : []), ...page.sessions.map(row => row.sessionId)])].slice(0, MAX_INDEX);
|
|
360
|
-
this.
|
|
366
|
+
this.cursor = page.nextCursor;
|
|
367
|
+
this.fetched = (more ? this.fetched : 0) + page.sessions.length;
|
|
361
368
|
this.moreSessions = page.hasMore && this.rows.length < MAX_INDEX;
|
|
362
369
|
this.indexError = null;
|
|
363
370
|
const retained = new Set([...this.rows, ...this.details.keys(), ...(this.book.selectedId ? [this.book.selectedId] : [])]);
|
|
@@ -463,7 +470,7 @@ export class CodeController {
|
|
|
463
470
|
await Promise.all([this.readCatalog(), this.readIndex(), id ? this.sync(id, true) : Promise.resolve(), this.readGit()]);
|
|
464
471
|
};
|
|
465
472
|
loadMoreSessions = async (): Promise<void> => { if (this.moreSessions && !this.indexLoading) await this.readIndex(true); };
|
|
466
|
-
setFilter = (filter: CodeSessionFilter): void => { this.filter = { ...filter }; this.
|
|
473
|
+
setFilter = (filter: CodeSessionFilter): void => { this.filter = { ...filter }; this.cursor = null; this.fetched = 0; this.rows = []; this.moreSessions = false; this.notify(); void this.readIndex(); };
|
|
467
474
|
// Explicitly abandon only the local uncertain attempt. The server session may exist.
|
|
468
475
|
startAnotherSession = (): void => {
|
|
469
476
|
const previous = this.book.fresh;
|
|
@@ -29,7 +29,7 @@ export interface CodeGitInfo {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface CodeListOptions {
|
|
32
|
-
scope?: 'all' | 'cwd'; cwd?: string; archived?: boolean;
|
|
32
|
+
scope?: 'all' | 'cwd'; cwd?: string; archived?: boolean; cursor?: string; limit?: number;
|
|
33
33
|
}
|
|
34
34
|
export interface CodeSessionClient {
|
|
35
35
|
listSessions(options?: CodeListOptions, signal?: AbortSignal): Promise<CodeSessionPage>;
|
|
@@ -4,13 +4,12 @@ import type { CodeSessionInfo } from '../../../../src/code-mode/wire';
|
|
|
4
4
|
* How the session list is ordered and grouped, kept apart from rendering so the
|
|
5
5
|
* rules can be stated and tested on their own.
|
|
6
6
|
*
|
|
7
|
-
* The server
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* belongs.
|
|
7
|
+
* The server pages sessions by `created_at DESC` — the stable key that keeps a
|
|
8
|
+
* paged read complete while `last_used_at` moves under it. Order here is
|
|
9
|
+
* anchored to that same creation order: a row holds its place until something
|
|
10
|
+
* actually changes about it, so the list moves when a session is created or
|
|
11
|
+
* archived and not merely because it is busy. Which session is running is
|
|
12
|
+
* carried by the row's own status, which is where a changing fact belongs.
|
|
14
13
|
*/
|
|
15
14
|
export type CodeSessionSection = 'active' | 'archived';
|
|
16
15
|
|
|
@@ -17,3 +17,11 @@ export function readInitialSidebarMode(search: string): DashboardSidebarMode | n
|
|
|
17
17
|
export function readTrayRemindersMode(search: string): boolean {
|
|
18
18
|
return new URLSearchParams(search).get('tray') === '1';
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
/** `?port=<n>` from the menu bar "open instance" item; null when absent or invalid. */
|
|
22
|
+
export function readInitialSelectedPort(search: string): number | null {
|
|
23
|
+
const raw = new URLSearchParams(search).get('port');
|
|
24
|
+
if (raw === null || !/^\d{1,5}$/.test(raw)) return null;
|
|
25
|
+
const port = Number(raw);
|
|
26
|
+
return port >= 1 && port <= 65535 ? port : null;
|
|
27
|
+
}
|
|
@@ -45,6 +45,8 @@ export function stripServerOwnedSettingsFields(settings: Record<string, unknown>
|
|
|
45
45
|
nativeTransportMigration: _nativeTransportMigration,
|
|
46
46
|
maxConcurrentDefaultMigration: _maxConcurrentDefaultMigration,
|
|
47
47
|
slackEnvironmentVariables: _slackEnvironmentVariables,
|
|
48
|
+
telegramEnvironmentVariables: _telegramEnvironmentVariables,
|
|
49
|
+
discordEnvironmentVariables: _discordEnvironmentVariables,
|
|
48
50
|
...userOwned
|
|
49
51
|
} = settings;
|
|
50
52
|
return userOwned;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
PageOffline,
|
|
11
11
|
usePageSnapshot,
|
|
12
12
|
} from './page-shell';
|
|
13
|
-
import { parsePermissionsValue } from './Permissions';
|
|
13
|
+
import { parsePermissionsValue, permissionsEditMode } from './Permissions';
|
|
14
14
|
import { RuntimeHeader } from './components/agent/RuntimeHeader';
|
|
15
15
|
import { PermissionQuickSection } from './components/agent/PermissionQuickSection';
|
|
16
16
|
import { FlushAgentSection } from './components/agent/FlushAgentSection';
|
|
@@ -124,7 +124,10 @@ type RuntimeDraft = {
|
|
|
124
124
|
model: string;
|
|
125
125
|
effort: string;
|
|
126
126
|
workingDir: string;
|
|
127
|
-
|
|
127
|
+
// 'auto' | 'safe' | string[] when the stored policy is a known shape; the raw
|
|
128
|
+
// stored value otherwise, so the editor can render it as invalid rather than
|
|
129
|
+
// silently selecting Auto (#788).
|
|
130
|
+
permissions: unknown;
|
|
128
131
|
};
|
|
129
132
|
|
|
130
133
|
export default function Agent({ port, client, dirty, registerSave }: SettingsPageProps) {
|
|
@@ -277,6 +280,7 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
|
|
|
277
280
|
const cliKeys = Object.keys(state.data.perCli || {});
|
|
278
281
|
const cli = state.data.cli || cliKeys[0] || '';
|
|
279
282
|
const permissions = parsePermissionsValue(state.data.permissions);
|
|
283
|
+
const permissionsMode = permissionsEditMode(state.data.permissions);
|
|
280
284
|
const meta = metaFor(cli, cliMeta);
|
|
281
285
|
setDraft({
|
|
282
286
|
cli,
|
|
@@ -286,8 +290,12 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
|
|
|
286
290
|
workingDir: state.data.workingDir || '',
|
|
287
291
|
// Preserve 'safe' rather than collapsing it: an untouched Safe instance must not
|
|
288
292
|
// become Auto (YOLO) as a side effect of saving something unrelated on this page.
|
|
289
|
-
|
|
290
|
-
|
|
293
|
+
// An unrecognized stored value stays in the draft as-is so the editor can show
|
|
294
|
+
// a recoverable invalid state instead of silently selecting Auto (#788).
|
|
295
|
+
permissions: permissionsMode === 'invalid' ? state.data.permissions
|
|
296
|
+
: permissions.mode === 'custom' ? permissions.tokens
|
|
297
|
+
: permissions.mode === 'safe' ? 'safe'
|
|
298
|
+
: 'auto',
|
|
291
299
|
});
|
|
292
300
|
}, [cliMeta, state]);
|
|
293
301
|
|
|
@@ -570,7 +578,7 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
|
|
|
570
578
|
setEntry('permissions', {
|
|
571
579
|
value: next,
|
|
572
580
|
original: settingsData.permissions ?? 'auto',
|
|
573
|
-
valid: next === 'auto' || next.length > 0,
|
|
581
|
+
valid: next === 'auto' || next === 'safe' || next.length > 0,
|
|
574
582
|
});
|
|
575
583
|
}}
|
|
576
584
|
/>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// discord.allowBots
|
|
12
12
|
// discord.mentionOnly
|
|
13
13
|
|
|
14
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
14
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
import type { SettingsPageProps, DirtyEntry } from '../types';
|
|
16
16
|
import { ToggleField, SecretField, ChipListField, TextField } from '../fields';
|
|
17
17
|
import {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
usePageSnapshot,
|
|
23
23
|
} from './page-shell';
|
|
24
24
|
import { expandPatch } from './path-utils';
|
|
25
|
+
import { slackText } from './components/SlackSetup';
|
|
25
26
|
import { HealthBadge, interpretDiscordHealth } from './components/HealthBadge';
|
|
26
27
|
import type { MessengerChannel } from './components/ChannelEnablementControl';
|
|
27
28
|
import { TransportStatusChips } from './components/TransportStatusChips';
|
|
@@ -46,6 +47,7 @@ type DiscordBlock = {
|
|
|
46
47
|
type DiscordSnapshot = {
|
|
47
48
|
channel?: MessengerChannel;
|
|
48
49
|
discord?: DiscordBlock;
|
|
50
|
+
discordEnvironmentVariables?: string[];
|
|
49
51
|
messaging?: MessagingBlock;
|
|
50
52
|
[key: string]: unknown;
|
|
51
53
|
};
|
|
@@ -62,13 +64,16 @@ const DISCORD_KEYS = [
|
|
|
62
64
|
'discord.mentionOnly',
|
|
63
65
|
] as const;
|
|
64
66
|
|
|
67
|
+
const CONNECTION_KEYS = ['discord.enabled', 'discord.token', 'discord.guildId', 'discord.channelIds'];
|
|
68
|
+
|
|
65
69
|
/** Discord IDs are snowflakes — long numeric strings. Be lenient: 16+ digits typical. */
|
|
66
70
|
export function isValidSnowflake(chip: string): boolean {
|
|
67
71
|
if (!chip) return false;
|
|
68
72
|
return /^\d{5,32}$/.test(chip.trim());
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
export default function ChannelsDiscord({ port, client, dirty, registerSave }: SettingsPageProps) {
|
|
75
|
+
export default function ChannelsDiscord({ port, client, dirty, registerSave, manager }: SettingsPageProps) {
|
|
76
|
+
const t = useMemo(() => slackText(manager?.ui.locale ?? document.documentElement.lang), [manager?.ui.locale]);
|
|
72
77
|
const { state, refresh, setData } = usePageSnapshot<DiscordSnapshot>(client, '/api/settings');
|
|
73
78
|
|
|
74
79
|
const [setupOpen, setSetupOpen] = useState(false);
|
|
@@ -114,10 +119,22 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
|
|
|
114
119
|
return state.data.discord || {};
|
|
115
120
|
}, [state]);
|
|
116
121
|
|
|
122
|
+
const environmentVariables = state.kind === 'ready' && Array.isArray(state.data.discordEnvironmentVariables)
|
|
123
|
+
? state.data.discordEnvironmentVariables
|
|
124
|
+
: [];
|
|
125
|
+
const environmentManaged = environmentVariables.length > 0;
|
|
126
|
+
const savePolicyRef = useRef({ environmentManaged, environmentVariables, t });
|
|
127
|
+
useLayoutEffect(() => { savePolicyRef.current = { environmentManaged, environmentVariables, t }; },
|
|
128
|
+
[environmentManaged, environmentVariables, t]);
|
|
129
|
+
|
|
117
130
|
const onSave = useCallback(async () => {
|
|
118
131
|
if (setupOpen) throw new Error('Finish channel setup before saving settings.');
|
|
119
132
|
const bundle = dirty.saveBundle();
|
|
120
133
|
if (Object.keys(bundle).length === 0) return;
|
|
134
|
+
const policy = savePolicyRef.current;
|
|
135
|
+
if (policy.environmentManaged && CONNECTION_KEYS.some(key => key in bundle)) {
|
|
136
|
+
throw new Error(policy.t('settings.discord.managedByEnvironment', { variables: policy.environmentVariables.join(', ') }));
|
|
137
|
+
}
|
|
121
138
|
const patch = expandPatch(bundle);
|
|
122
139
|
const updated = await client.put<DiscordSnapshot>('/api/settings', patch);
|
|
123
140
|
const fresh = (updated && typeof updated === 'object' && 'data' in updated
|
|
@@ -189,11 +206,14 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
|
|
|
189
206
|
<TransportStatusChips client={client} channel="discord" />
|
|
190
207
|
</SettingsSection>
|
|
191
208
|
|
|
192
|
-
<SettingsSection title="Discord" hint=
|
|
209
|
+
<SettingsSection title="Discord" hint={environmentManaged
|
|
210
|
+
? t('settings.discord.managedByEnvironment', { variables: environmentVariables.join(', ') })
|
|
211
|
+
: 'Bot token, guild + channels, forwarding rules.'}>
|
|
193
212
|
<ToggleField
|
|
194
213
|
id="dc-enabled"
|
|
195
214
|
label="Discord enabled"
|
|
196
215
|
value={enabled}
|
|
216
|
+
disabled={environmentManaged}
|
|
197
217
|
onChange={(next) => {
|
|
198
218
|
setEnabled(next);
|
|
199
219
|
setEntry('discord.enabled', {
|
|
@@ -208,6 +228,7 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
|
|
|
208
228
|
label="Bot token"
|
|
209
229
|
value={token}
|
|
210
230
|
placeholder={tokenPlaceholder}
|
|
231
|
+
disabled={environmentManaged}
|
|
211
232
|
onChange={(next) => {
|
|
212
233
|
setToken(next);
|
|
213
234
|
if (next.length === 0) {
|
|
@@ -227,6 +248,7 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
|
|
|
227
248
|
value={guildId}
|
|
228
249
|
placeholder="123456789012345678"
|
|
229
250
|
error={guildError}
|
|
251
|
+
disabled={environmentManaged}
|
|
230
252
|
onChange={(next) => {
|
|
231
253
|
setGuildId(next);
|
|
232
254
|
const valid = next.length === 0 || isValidSnowflake(next);
|
|
@@ -243,6 +265,7 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
|
|
|
243
265
|
value={channelIds}
|
|
244
266
|
placeholder="987654321098765432"
|
|
245
267
|
error={channelIdsError}
|
|
268
|
+
disabled={environmentManaged}
|
|
246
269
|
onChange={(next) => {
|
|
247
270
|
setChannelIds(next);
|
|
248
271
|
const allValid = next.every(isValidSnowflake);
|
|
@@ -307,7 +330,7 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
|
|
|
307
330
|
/>
|
|
308
331
|
</SettingsSection>
|
|
309
332
|
</fieldset>
|
|
310
|
-
<ChannelSetupEntry channel="discord" client={client} dirty={dirty}
|
|
333
|
+
<ChannelSetupEntry channel="discord" client={client} dirty={dirty} disabled={environmentManaged}
|
|
311
334
|
open={setupOpen} onOpenChange={setSetupOpen} onSaved={async () => setData(await client.get<DiscordSnapshot>('/api/settings'))} />
|
|
312
335
|
</form>
|
|
313
336
|
);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// telegram.forwardAll
|
|
10
10
|
// telegram.mentionOnly
|
|
11
11
|
|
|
12
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
12
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
13
13
|
import type { SettingsPageProps, DirtyEntry } from '../types';
|
|
14
14
|
import { ToggleField, SecretField, ChipListField } from '../fields';
|
|
15
15
|
import {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
usePageSnapshot,
|
|
21
21
|
} from './page-shell';
|
|
22
22
|
import { expandPatch } from './path-utils';
|
|
23
|
+
import { slackText } from './components/SlackSetup';
|
|
23
24
|
import { HealthBadge, interpretTelegramProbe } from './components/HealthBadge';
|
|
24
25
|
import type { MessengerChannel } from './components/ChannelEnablementControl';
|
|
25
26
|
import { TransportStatusChips } from './components/TransportStatusChips';
|
|
@@ -42,6 +43,7 @@ type TelegramBlock = {
|
|
|
42
43
|
type TelegramSnapshot = {
|
|
43
44
|
channel?: MessengerChannel;
|
|
44
45
|
telegram?: TelegramBlock;
|
|
46
|
+
telegramEnvironmentVariables?: string[];
|
|
45
47
|
messaging?: MessagingBlock;
|
|
46
48
|
[key: string]: unknown;
|
|
47
49
|
};
|
|
@@ -56,6 +58,8 @@ const TELEGRAM_KEYS = [
|
|
|
56
58
|
'telegram.mentionOnly',
|
|
57
59
|
] as const;
|
|
58
60
|
|
|
61
|
+
const CONNECTION_KEYS = ['telegram.enabled', 'telegram.token', 'telegram.allowedChatIds'];
|
|
62
|
+
|
|
59
63
|
// ── pure helpers (exported for tests) ────────────────────────────────
|
|
60
64
|
|
|
61
65
|
/** Strict numeric-string check: digits, optional leading minus. No floats, no "1e9". */
|
|
@@ -97,7 +101,8 @@ export function chipsToChatIds(chips: ReadonlyArray<string>): number[] {
|
|
|
97
101
|
|
|
98
102
|
// ── component ────────────────────────────────────────────────────────
|
|
99
103
|
|
|
100
|
-
export default function ChannelsTelegram({ port, client, dirty, registerSave }: SettingsPageProps) {
|
|
104
|
+
export default function ChannelsTelegram({ port, client, dirty, registerSave, manager }: SettingsPageProps) {
|
|
105
|
+
const t = useMemo(() => slackText(manager?.ui.locale ?? document.documentElement.lang), [manager?.ui.locale]);
|
|
101
106
|
const { state, refresh, setData } = usePageSnapshot<TelegramSnapshot>(client, '/api/settings');
|
|
102
107
|
|
|
103
108
|
const [setupOpen, setSetupOpen] = useState(false);
|
|
@@ -142,10 +147,22 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
|
|
|
142
147
|
return state.data.telegram || {};
|
|
143
148
|
}, [state]);
|
|
144
149
|
|
|
150
|
+
const environmentVariables = state.kind === 'ready' && Array.isArray(state.data.telegramEnvironmentVariables)
|
|
151
|
+
? state.data.telegramEnvironmentVariables
|
|
152
|
+
: [];
|
|
153
|
+
const environmentManaged = environmentVariables.length > 0;
|
|
154
|
+
const savePolicyRef = useRef({ environmentManaged, environmentVariables, t });
|
|
155
|
+
useLayoutEffect(() => { savePolicyRef.current = { environmentManaged, environmentVariables, t }; },
|
|
156
|
+
[environmentManaged, environmentVariables, t]);
|
|
157
|
+
|
|
145
158
|
const onSave = useCallback(async () => {
|
|
146
159
|
if (setupOpen) throw new Error('Finish channel setup before saving settings.');
|
|
147
160
|
const bundle = dirty.saveBundle();
|
|
148
161
|
if (Object.keys(bundle).length === 0) return;
|
|
162
|
+
const policy = savePolicyRef.current;
|
|
163
|
+
if (policy.environmentManaged && CONNECTION_KEYS.some(key => key in bundle)) {
|
|
164
|
+
throw new Error(policy.t('settings.telegram.managedByEnvironment', { variables: policy.environmentVariables.join(', ') }));
|
|
165
|
+
}
|
|
149
166
|
const patch = expandPatch(bundle);
|
|
150
167
|
const updated = await client.put<TelegramSnapshot>('/api/settings', patch);
|
|
151
168
|
const fresh = (updated && typeof updated === 'object' && 'data' in updated
|
|
@@ -212,11 +229,14 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
|
|
|
212
229
|
<TransportStatusChips client={client} channel="telegram" />
|
|
213
230
|
</SettingsSection>
|
|
214
231
|
|
|
215
|
-
<SettingsSection title="Telegram" hint=
|
|
232
|
+
<SettingsSection title="Telegram" hint={environmentManaged
|
|
233
|
+
? t('settings.telegram.managedByEnvironment', { variables: environmentVariables.join(', ') })
|
|
234
|
+
: 'Bot token, allow-list, and forwarding rules.'}>
|
|
216
235
|
<ToggleField
|
|
217
236
|
id="tg-enabled"
|
|
218
237
|
label="Telegram enabled"
|
|
219
238
|
value={enabled}
|
|
239
|
+
disabled={environmentManaged}
|
|
220
240
|
onChange={(next) => {
|
|
221
241
|
setEnabled(next);
|
|
222
242
|
setEntry('telegram.enabled', {
|
|
@@ -231,6 +251,7 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
|
|
|
231
251
|
label="Bot token"
|
|
232
252
|
value={token}
|
|
233
253
|
placeholder={tokenPlaceholder}
|
|
254
|
+
disabled={environmentManaged}
|
|
234
255
|
onChange={(next) => {
|
|
235
256
|
setToken(next);
|
|
236
257
|
// Empty input means "leave existing token alone"; only
|
|
@@ -252,6 +273,7 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
|
|
|
252
273
|
value={chips}
|
|
253
274
|
placeholder="123456789"
|
|
254
275
|
error={chipsError}
|
|
276
|
+
disabled={environmentManaged}
|
|
255
277
|
onChange={(next) => {
|
|
256
278
|
setChips(next);
|
|
257
279
|
const allValid = next.every(isValidChatId);
|
|
@@ -303,7 +325,7 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
|
|
|
303
325
|
/>
|
|
304
326
|
</SettingsSection>
|
|
305
327
|
</fieldset>
|
|
306
|
-
<ChannelSetupEntry channel="telegram" client={client} dirty={dirty}
|
|
328
|
+
<ChannelSetupEntry channel="telegram" client={client} dirty={dirty} disabled={environmentManaged}
|
|
307
329
|
open={setupOpen} onOpenChange={setSetupOpen} onSaved={async () => setData(await client.get<TelegramSnapshot>('/api/settings'))} />
|
|
308
330
|
</form>
|
|
309
331
|
);
|