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.
Files changed (100) hide show
  1. package/README.ja.md +376 -339
  2. package/README.ko.md +338 -393
  3. package/README.md +163 -508
  4. package/README.zh-CN.md +365 -376
  5. package/dist/server.js +3 -0
  6. package/dist/server.js.map +1 -1
  7. package/dist/src/agent/pi-runtime.js +37 -18
  8. package/dist/src/agent/pi-runtime.js.map +1 -1
  9. package/dist/src/agent/runtime/acp/permissions.js +2 -3
  10. package/dist/src/agent/runtime/acp/permissions.js.map +1 -1
  11. package/dist/src/agent/runtime/pi-turn.js +7 -0
  12. package/dist/src/agent/runtime/pi-turn.js.map +1 -1
  13. package/dist/src/agent/spawn/line-buffer.js +84 -13
  14. package/dist/src/agent/spawn/line-buffer.js.map +1 -1
  15. package/dist/src/agent/spawn.js +26 -15
  16. package/dist/src/agent/spawn.js.map +1 -1
  17. package/dist/src/code-mode/host.js +34 -5
  18. package/dist/src/code-mode/host.js.map +1 -1
  19. package/dist/src/code-mode/manager.js +24 -7
  20. package/dist/src/code-mode/manager.js.map +1 -1
  21. package/dist/src/code-mode/providers/provider-live-models.js +3 -1
  22. package/dist/src/code-mode/providers/provider-live-models.js.map +1 -1
  23. package/dist/src/code-mode/store.js +31 -6
  24. package/dist/src/code-mode/store.js.map +1 -1
  25. package/dist/src/core/config.js +119 -17
  26. package/dist/src/core/config.js.map +1 -1
  27. package/dist/src/core/runtime-settings.js +7 -1
  28. package/dist/src/core/runtime-settings.js.map +1 -1
  29. package/dist/src/core/settings-merge.js +9 -0
  30. package/dist/src/core/settings-merge.js.map +1 -1
  31. package/dist/src/core/settings-watch.js +19 -1
  32. package/dist/src/core/settings-watch.js.map +1 -1
  33. package/dist/src/discord/bot.js +6 -4
  34. package/dist/src/discord/bot.js.map +1 -1
  35. package/dist/src/discord/send-only-client.js +37 -3
  36. package/dist/src/discord/send-only-client.js.map +1 -1
  37. package/dist/src/manager/memory/embedding/sync.js +6 -0
  38. package/dist/src/manager/memory/embedding/sync.js.map +1 -1
  39. package/dist/src/manager/routes/dashboard-memory.js +20 -7
  40. package/dist/src/manager/routes/dashboard-memory.js.map +1 -1
  41. package/dist/src/manager/server.js +8 -0
  42. package/dist/src/manager/server.js.map +1 -1
  43. package/dist/src/manager/shutdown.js +29 -3
  44. package/dist/src/manager/shutdown.js.map +1 -1
  45. package/dist/src/manager/worker-events.js +70 -8
  46. package/dist/src/manager/worker-events.js.map +1 -1
  47. package/dist/src/routes/code-native.js +23 -3
  48. package/dist/src/routes/code-native.js.map +1 -1
  49. package/dist/src/routes/events.js +4 -0
  50. package/dist/src/routes/events.js.map +1 -1
  51. package/dist/src/routes/settings.js +45 -1
  52. package/dist/src/routes/settings.js.map +1 -1
  53. package/dist/src/routes/sse-connections.js +31 -0
  54. package/dist/src/routes/sse-connections.js.map +1 -0
  55. package/dist/src/shared/permissions.js +47 -0
  56. package/dist/src/shared/permissions.js.map +1 -0
  57. package/package.json +1 -1
  58. package/public/dist/.vite/manifest.json +31 -31
  59. package/public/dist/assets/AdvancedExport-B7h_ZvBu.js +1 -0
  60. package/public/dist/assets/Agent-DnbZJP7b.js +1 -0
  61. package/public/dist/assets/{ChannelEnablementControl-B2tq8Ji4.js → ChannelEnablementControl-Dl0uYx38.js} +5 -5
  62. package/public/dist/assets/ChannelSetupEntry-DxtOrN01.js +1 -0
  63. package/public/dist/assets/ChannelsDiscord-BEGGwd_3.js +1 -0
  64. package/public/dist/assets/{ChannelsSlack-B9FV0ftr.js → ChannelsSlack-BryC2Ahr.js} +1 -1
  65. package/public/dist/assets/ChannelsTelegram-CGSpaImJ.js +1 -0
  66. package/public/dist/assets/CodeCanvas-DpkFi5Au.js +2 -0
  67. package/public/dist/assets/Permissions-BwiMEoTV.js +1 -0
  68. package/public/dist/assets/Permissions-CodIW-Yd.js +1 -0
  69. package/public/dist/assets/{manager-C5cU1joU.js → manager-Bi_xw7TW.js} +2 -2
  70. package/public/dist/assets/{settings-CA2LQDGh.js → settings-W4KG9ivu.js} +1 -1
  71. package/public/dist/assets/{settings-embedding-oGvFayh-.js → settings-embedding-BfWzWKMM.js} +1 -1
  72. package/public/dist/manager/index.html +1 -1
  73. package/public/dist/settings/index.html +1 -1
  74. package/public/js/features/settings-discord.ts +39 -5
  75. package/public/js/features/settings-telegram.ts +38 -4
  76. package/public/js/features/settings-types.ts +2 -0
  77. package/public/locales/en.json +2 -0
  78. package/public/locales/ja.json +2 -0
  79. package/public/locales/ko.json +2 -0
  80. package/public/locales/zh.json +2 -0
  81. package/public/manager/src/App.tsx +4 -1
  82. package/public/manager/src/code/code-controller-runtime.ts +13 -6
  83. package/public/manager/src/code/code-session-client.ts +1 -1
  84. package/public/manager/src/code/session-order.ts +6 -7
  85. package/public/manager/src/dashboard-url-state.ts +8 -0
  86. package/public/manager/src/settings/pages/AdvancedExport.tsx +2 -0
  87. package/public/manager/src/settings/pages/Agent.tsx +13 -5
  88. package/public/manager/src/settings/pages/ChannelsDiscord.tsx +27 -4
  89. package/public/manager/src/settings/pages/ChannelsTelegram.tsx +26 -4
  90. package/public/manager/src/settings/pages/Permissions.tsx +50 -27
  91. package/public/manager/src/settings/pages/components/ChannelSetupEntry.tsx +4 -3
  92. package/public/manager/src/settings/pages/components/agent/PermissionQuickSection.tsx +10 -2
  93. package/public/dist/assets/AdvancedExport-BNMjYkw6.js +0 -1
  94. package/public/dist/assets/Agent-D4g8TPjm.js +0 -1
  95. package/public/dist/assets/ChannelSetupEntry-zKYImWXx.js +0 -1
  96. package/public/dist/assets/ChannelsDiscord-CWRKV-Zl.js +0 -1
  97. package/public/dist/assets/ChannelsTelegram-BdK-fTPI.js +0 -1
  98. package/public/dist/assets/CodeCanvas-D5jp8-t5.js +0 -2
  99. package/public/dist/assets/Permissions-Bh947tUG.js +0 -1
  100. 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-C5cU1joU.js"></script>
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-CA2LQDGh.js"></script>
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 (dc.token && dcToken) dcToken.value = dc.token;
83
+ if (dcToken) dcToken.value = environmentManaged ? '' : dc.token || '';
48
84
  const dcGuildId = document.getElementById('dcGuildId') as HTMLInputElement | null;
49
- if (dc.guildId && dcGuildId) dcGuildId.value = dc.guildId;
85
+ if (dcGuildId) dcGuildId.value = environmentManaged ? '' : dc.guildId || '';
50
86
  const dcChannelIds = document.getElementById('dcChannelIds') as HTMLInputElement | null;
51
- if (dc.channelIds?.length && dcChannelIds) {
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 (tg.token && tgToken) tgToken.value = tg.token;
76
+ if (tgToken) tgToken.value = environmentManaged ? '' : tg.token || '';
41
77
  const tgChatIds = document.getElementById('tgChatIds') as HTMLInputElement | null;
42
- if (tg.allowedChatIds?.length && tgChatIds) {
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;
@@ -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",
@@ -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 をコピー",
@@ -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 복사",
@@ -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 offset = 0;
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
- const offset = more ? this.offset : 0;
342
- const limit = more ? INDEX_PAGE_SIZE : Math.min(MAX_INDEX, Math.max(INDEX_PAGE_SIZE, this.offset));
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, offset,
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.offset = page.offset + page.sessions.length;
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.offset = 0; this.rows = []; this.moreSessions = false; this.notify(); void this.readIndex(); };
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; offset?: number; limit?: number;
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 returns sessions by `last_used_at DESC`, which means answering a
8
- * prompt in the third session moves it to the top while the reader is looking
9
- * at it. Order here is anchored to creation instead: a row holds its place
10
- * until something actually changes about it, so the list moves when a session
11
- * is created or archived and not merely because it is busy. Which session is
12
- * running is carried by the row's own status, which is where a changing fact
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
- permissions: 'auto' | 'safe' | string[];
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
- permissions: permissions.mode === 'custom' ? permissions.tokens
290
- : permissions.mode === 'safe' ? 'safe' : 'auto',
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="Bot token, guild + channels, forwarding rules.">
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="Bot token, allow-list, and forwarding rules.">
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
  );