You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/21 13:02:25 UTC

(maka) 03/06: fix(desktop): keep the composer's new-task permission choice

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch refactor/permission-single-authority
in repository https://gitbox.apache.org/repos/asf/maka.git

commit fa933eaab65c157f46732b81c7e2f4e2b959b72a
Author: AstroHan <le...@outlook.com>
AuthorDate: Fri Aug 21 16:02:27 2026 +0800

    fix(desktop): keep the composer's new-task permission choice
    
    Picking full access in the composer before starting a task lasted exactly
    one draft: `useNewTaskChoice` held it in an in-memory Map keyed by the draft,
    so the next new task was back on Auto. The setting that does persist —
    `chatDefaults.permissionMode` — was only reachable from Settings, which is
    where users were not looking.
    
    Before a Session exists, "the mode this task will start in" and "the
    configured default" are the same fact, so the draft-local copy could only
    ever be the stale one. The picker now reads the Host's value and writes it,
    which is what makes the choice survive to the next task.
    
    It writes to the Host that would run the task rather than to whichever Host
    is otherwise selected. Those differ once more than one Host is connected,
    and the mode shown in the composer belongs to the first one.
    
    The shell's second copy of the same setting goes with it. `app-shell` read
    the selected Host's `chatDefaults` in one place and a global copy hydrated
    from the *default* Host in another, so with several Hosts connected the
    placeholder session view could name a different Host's mode than the picker
    directly above it. Settings now asks the shell to re-read the value instead
    of handing it a third copy.
    
    Renderer behavior has no unit-test surface in this app (desktop tests cover
    `main`), so this is typecheck- and lint-verified only; the persistence is
    observable through the existing Settings picker.
    
    Refs #3385
    
    Generated-by: Claude Code
---
 apps/desktop/src/renderer/app-shell-overlays.tsx   | 12 +++++--
 .../renderer/app-shell-session-settings-actions.ts |  5 +--
 apps/desktop/src/renderer/app-shell.tsx            | 41 ++++++++++++++++------
 apps/desktop/src/renderer/use-shell-appearance.ts  | 30 +++++++---------
 4 files changed, 54 insertions(+), 34 deletions(-)

diff --git a/apps/desktop/src/renderer/app-shell-overlays.tsx b/apps/desktop/src/renderer/app-shell-overlays.tsx
index a92d5ef52..61ca22989 100644
--- a/apps/desktop/src/renderer/app-shell-overlays.tsx
+++ b/apps/desktop/src/renderer/app-shell-overlays.tsx
@@ -43,7 +43,13 @@ export function AppShellOverlays(props: {
   setUiLocalePreference: (preference: UiLocalePreference) => void;
   uiLocaleUpdateGate: UiLocaleUpdateGate;
   setUserLabel(userLabel: string): void;
-  setDefaultPermissionMode(mode: ChatDefaultPermissionMode): void;
+  /**
+   * Settings changed a chat default the composer also shows. The shell
+   * re-reads it from the Host rather than being handed the new value: the
+   * Host owns it, and a value passed along here would be a second copy that
+   * can disagree the moment anything else writes the setting.
+   */
+  refreshChatDefaults(): void;
   settingsRequestedSection: SettingsSection | undefined;
   settingsProviderCatalogOpen: boolean;
   settingsConnectionDetailSlug: string | undefined;
@@ -85,7 +91,7 @@ export function AppShellOverlays(props: {
     setUiLocalePreference,
     uiLocaleUpdateGate,
     setUserLabel,
-    setDefaultPermissionMode,
+    refreshChatDefaults,
     themePalette,
     themePref,
     onExternalSessionImported,
@@ -107,7 +113,7 @@ export function AppShellOverlays(props: {
             onUiLocalePreferenceChange={setUiLocalePreference}
             uiLocaleUpdateGate={uiLocaleUpdateGate}
             onUserLabelChange={setUserLabel}
-            onDefaultPermissionModeChange={setDefaultPermissionMode}
+            onDefaultPermissionModeChange={() => refreshChatDefaults()}
             requestedSection={settingsRequestedSection}
             openProviderCatalog={settingsProviderCatalogOpen}
             initialConnectionSlug={settingsConnectionDetailSlug}
diff --git a/apps/desktop/src/renderer/app-shell-session-settings-actions.ts b/apps/desktop/src/renderer/app-shell-session-settings-actions.ts
index 0fb3cdb03..b6a7e4d59 100644
--- a/apps/desktop/src/renderer/app-shell-session-settings-actions.ts
+++ b/apps/desktop/src/renderer/app-shell-session-settings-actions.ts
@@ -43,7 +43,8 @@ export function createAppShellSessionSettingsActions(deps: {
     model: { llmConnectionSlug: string; model: string };
   }) => void;
   sessionsRef: RefBox<DesktopSessionSummary[]>;
-  setNewTaskPermissionMode: (mode: ChatDefaultPermissionMode) => void;
+  /** Persists the chat default; awaited so a failure surfaces as one. */
+  setNewTaskPermissionMode: (mode: ChatDefaultPermissionMode) => void | Promise<void>;
   setPendingPermissionModeBySession: BooleanRecordUpdater;
   setPendingSessionModelBySession: BooleanRecordUpdater;
   setSessions: (
@@ -122,7 +123,7 @@ export function createAppShellSessionSettingsActions(deps: {
           prev.map((session) => (session.id === sessionId ? next : session)),
         );
       } else {
-        setNewTaskPermissionMode(mode);
+        await setNewTaskPermissionMode(mode);
       }
       toastApi.success(
         copy.permissionSwitched(copy.permissionLabels[nextMode]),
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index 4e854995c..d6c442c4c 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -87,7 +87,6 @@ import {
   stageCompanionQuote,
 } from './quote-companion-panel-state';
 import { UNRESOLVED_NEW_TASK_DRAFT_KEY } from './new-task-reload-intent';
-import { useNewTaskChoice } from './use-new-task-choice';
 import { sideChatTitleFromPrompt } from './side-chat-command';
 import { parseDesktopSlashCommand } from './desktop-slash-command';
 import {
@@ -427,8 +426,6 @@ function AppShellContent({
   const [scheduledTaskCreateRequestNonce, setScheduledTaskCreateRequestNonce] = useState(0);
   const [pendingCollaborationModeBySession, setPendingCollaborationModeBySession] = useState<Record<string, boolean>>({});
   const [pendingOrchestrationModeBySession, setPendingOrchestrationModeBySession] = useState<Record<string, boolean>>({});
-  const [newTaskPermissionChoice, setNewTaskPermissionChoice] =
-    useNewTaskChoice<ChatDefaultPermissionMode>(currentNewTaskDraftKey);
   const [historyLoadPendingSessionId, setHistoryLoadPendingSessionId] = useState<string>();
   const [transcriptTurnIndex, setTranscriptTurnIndex] = useState<{
     sessionId: string;
@@ -551,8 +548,8 @@ function AppShellContent({
     uiLocaleUpdateGate,
     userLabel,
     setUserLabel,
-    defaultPermissionMode,
-    setDefaultPermissionMode,
+
+
     refreshShellSettings,
   } = useShellAppearance({
     toastApi,
@@ -565,11 +562,31 @@ function AppShellContent({
   const desktopConversationCopy = getDesktopConversationCopy(uiLocale);
   const terminalPanelCopy = desktopConversationCopy.terminalPanel;
   const workbarCopy = desktopConversationCopy.workbar;
+  /**
+   * What a new task starts in, read straight from the Host that would run it.
+   *
+   * There is no draft-local copy: before a Session exists, "the mode this
+   * task will start in" and "the configured default" are the same fact, and
+   * a second copy of it could only be the stale one. Picking a mode here
+   * therefore writes the setting — which is also why the choice survives to
+   * the next new task instead of lasting one draft.
+   */
   const newTaskPermissionMode =
-    newTaskPermissionChoice ??
-    newTask.selectedHost?.chatDefaults.permissionMode ??
-    'ask';
-  const setNewTaskPermissionMode = setNewTaskPermissionChoice;
+    newTask.selectedHost?.chatDefaults.permissionMode ?? 'ask';
+  const setNewTaskPermissionMode = useCallback(
+    async (mode: ChatDefaultPermissionMode) => {
+      const host = newTask.selectedHost;
+      // Write to the Host that would run the task, not to whichever Host is
+      // otherwise selected: with several connected they are not the same, and
+      // the mode shown here belongs to this one.
+      await window.maka.settings.update(
+        { chatDefaults: { permissionMode: mode } },
+        host ? { profileId: host.profile.id, hostId: host.hostId } : undefined,
+      );
+      await newTask.refresh();
+    },
+    [newTask],
+  );
   useEffect(() => {
     if (!isAppUpdateInstallFailure(appUpdateStatus)) {
       notifiedInstallErrorRef.current = null;
@@ -1306,7 +1323,7 @@ function AppShellContent({
       ? pendingSessionView({
           sessionId: activeId,
           name: shellCopy.newConversation,
-          permissionMode: defaultPermissionMode,
+          permissionMode: newTaskPermissionMode,
         })
       : undefined);
   // Each control reads its own field. There is nothing to project and nothing
@@ -3647,7 +3664,9 @@ function AppShellContent({
         setUiLocalePreference={setUiLocalePreference}
         uiLocaleUpdateGate={uiLocaleUpdateGate}
         setUserLabel={setUserLabel}
-        setDefaultPermissionMode={setDefaultPermissionMode}
+        refreshChatDefaults={() => {
+          void newTask.refresh();
+        }}
         settingsRequestedSection={settingsRequestedSection}
         settingsProviderCatalogOpen={settingsProviderCatalogOpen}
         settingsConnectionDetailSlug={settingsConnectionDetailSlug}
diff --git a/apps/desktop/src/renderer/use-shell-appearance.ts b/apps/desktop/src/renderer/use-shell-appearance.ts
index d22672816..07237ccdc 100644
--- a/apps/desktop/src/renderer/use-shell-appearance.ts
+++ b/apps/desktop/src/renderer/use-shell-appearance.ts
@@ -1,5 +1,5 @@
 import { useState, type Dispatch, type SetStateAction } from 'react';
-import type { ChatDefaultPermissionMode, ThemePalette, ThemePreference } from '@maka/core/settings';
+import type { ThemePalette, ThemePreference } from '@maka/core/settings';
 import type { ThinkingLevel } from '@maka/core/model-thinking';
 import type { UiLocale, UiLocalePreference } from '@maka/core/ui-locale';
 import { createUiLocaleUpdateGate } from './settings/ui-locale-update-gate';
@@ -11,15 +11,19 @@ type ToastApi = {
 };
 
 /**
- * Owns the appearance / personalization / default-permission-mode slice
- * (issue #1043): the theme + palette + UI-locale + user-label + default
- * permission mode state, plus the `refreshShellSettings` IPC pull. Desktop
- * appearance and locale are hydrated independently from the default Host's
- * chat defaults, so an offline Host cannot block the local UI preferences.
+ * Owns the appearance / personalization slice (issue #1043): the theme +
+ * palette + UI-locale + user-label state, plus the `refreshShellSettings` IPC
+ * pull. Desktop appearance and locale are hydrated independently from the
+ * default Host's chat defaults, so an offline Host cannot block the local UI
+ * preferences.
+ *
+ * The default permission mode is deliberately absent. It is per-Host and the
+ * composer reads it from the Host that would run the task; a copy hydrated
+ * here from the *default* Host would name a different Host's setting as soon
+ * as more than one is connected.
  *
  * `closeSettings` stays in AppShell: on close it calls `refreshShellSettings()`
- * so display mirrors (default permission mode) catch up without an app restart.
- * The full settings hydration lives here.
+ * so the remaining display mirrors catch up without an app restart.
  */
 export function useShellAppearance({
   toastApi,
@@ -36,13 +40,6 @@ export function useShellAppearance({
   const [themePalette, setThemePalette] = useState<ThemePalette>('default');
   const [uiLocaleUpdateGate] = useState(createUiLocaleUpdateGate);
   const [userLabel, setUserLabel] = useState<string>('');
-  // Settings -> 通用 -> 默认权限模式 - DISPLAY-ONLY mirror. The composer's
-  // picker shows it before the user makes a per-session choice; the actual
-  // authority for a new session's mode is main.ts's sessions:create fallback
-  // (the renderer omits permissionMode unless the user explicitly picked),
-  // so a stale value here can briefly mislabel the chip but never changes
-  // which mode a session is created with.
-  const [defaultPermissionMode, setDefaultPermissionMode] = useState<ChatDefaultPermissionMode>('ask');
   // undefined = the user expressed no preference, so each model uses its own.
   const [defaultThinkingLevel, setDefaultThinkingLevel] = useState<ThinkingLevel | undefined>(undefined);
 
@@ -91,7 +88,6 @@ export function useShellAppearance({
     if (runtimeHostResult.ok) {
       const next = runtimeHostResult.settings;
       setUserLabel(next.personalization.displayName ?? '');
-      setDefaultPermissionMode(next.chatDefaults.permissionMode ?? 'ask');
       setDefaultThinkingLevel(next.chatDefaults.thinkingLevel);
     }
   }
@@ -104,9 +100,7 @@ export function useShellAppearance({
     uiLocaleUpdateGate,
     userLabel,
     setUserLabel,
-    defaultPermissionMode,
     defaultThinkingLevel,
-    setDefaultPermissionMode,
     refreshShellSettings,
   };
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org