Skip to content

fix(settings): 防止凭据字段清空已有密钥#49

Merged
appergb merged 1 commit into
Open-Less:mainfrom
Cooper-X-Oak:codex/windows-credential-guard-pr
Apr 30, 2026
Merged

fix(settings): 防止凭据字段清空已有密钥#49
appergb merged 1 commit into
Open-Less:mainfrom
Cooper-X-Oak:codex/windows-credential-guard-pr

Conversation

@Cooper-X-Oak
Copy link
Copy Markdown
Contributor

@Cooper-X-Oak Cooper-X-Oak commented Apr 30, 2026

摘要

关联 fork 验证:Cooper-X-Oak#12

本 PR 是从 fork/dev 已验证批次拆出的第三个最小 upstream 维护项:修复设置页凭据字段在读取完成前可能把已有密钥保存为空值的问题。

fork/dev 先行验证

修复 / 新增 / 改进

  • 凭据字段读取期间禁用输入和默认填充按钮,避免加载完成前 blur 写入空值。
  • 增加 loaded / dirty 状态:只有用户实际修改后的字段才会保存。
  • 保留现有 debounce 保存行为,但 account 切换时会清理 pending debounce。
  • 读取、保存、复制失败时输出 console error,并在 UI 中显示失败状态。
  • 复制按钮改为显式 onCopy,剪贴板 API 不可用时不再静默失败。

兼容

  • 不包含:启动路径、热键 core、ASR、插入 fallback、权限状态。
  • 对现有用户 / 本地环境 / 构建流程的影响:只影响设置页凭据字段交互;不会迁移或改写已有凭据格式。

测试计划

Summary by Sourcery

Prevent settings credential fields from overwriting existing secrets with empty values and improve user feedback for credential operations.

Bug Fixes:

  • Ensure credential fields only save after credentials have finished loading and the user has actually modified the value.
  • Avoid pending debounced saves from a previous account when switching accounts in the settings page.
  • Stop attempting to copy credentials silently when the Clipboard API is unavailable, surfacing the failure instead.

Enhancements:

  • Introduce loaded/dirty/status state tracking for credential fields to control when saves occur and to show operation status badges for saving, saved, copied, and error states.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 30, 2026

Reviewer's Guide

Updates the settings credentials field component to be load-aware, track dirty state, debounce-safe across account changes, and provide explicit status feedback and error handling for read/save/copy operations to prevent accidentally overwriting existing secrets with empty values.

Sequence diagram for credential field load and save behavior

sequenceDiagram
    participant User
    participant CredentialField
    participant readCredential
    participant setCredential

    Note over CredentialField: Initial mount or account change
    CredentialField->>CredentialField: setLoaded(false)
    CredentialField->>CredentialField: setDirty(false)
    CredentialField->>CredentialField: setStatus(idle)
    CredentialField->>CredentialField: clearTimeout(debounceRef)
    CredentialField->>readCredential: readCredential(account)
    alt read success
        readCredential-->>CredentialField: value or null
        CredentialField->>CredentialField: setValue(value or empty)
        CredentialField->>CredentialField: setLoaded(true)
    else read failure
        readCredential--xCredentialField: error
        CredentialField->>CredentialField: console.error(read failure)
        CredentialField->>CredentialField: setLoaded(true)
        CredentialField->>CredentialField: setStatus(saveError)
    end

    User->>CredentialField: type in input
    CredentialField->>CredentialField: setValue(v)
    CredentialField->>CredentialField: setDirty(true)
    CredentialField->>CredentialField: clearTimeout(debounceRef)
    CredentialField->>CredentialField: debounce(save(v), 300ms)

    Note over CredentialField: Debounced save
    CredentialField->>setCredential: save(v) when debounce fires
    alt loaded is false
        setCredential-->>CredentialField: ignored (save returns)
    else loaded is true
        CredentialField->>CredentialField: setStatus(saving)
        alt save success
            setCredential-->>CredentialField: ok
            CredentialField->>CredentialField: setDirty(false)
            CredentialField->>CredentialField: setStatus(saved)
        else save failure
            setCredential--xCredentialField: error
            CredentialField->>CredentialField: console.error(save failure)
            CredentialField->>CredentialField: setStatus(saveError)
        end
        CredentialField->>CredentialField: setTimeout(setStatus(idle), 1600ms)
    end

    User->>CredentialField: blur input
    alt not loaded or not dirty
        CredentialField-->>User: no save
    else loaded and dirty
        CredentialField->>CredentialField: clearTimeout(debounceRef)
        CredentialField->>setCredential: save(current value)
    end
Loading

Sequence diagram for credential field copy behavior

sequenceDiagram
    participant User
    participant CredentialField
    participant ClipboardAPI as navigator.clipboard

    User->>CredentialField: click copy button
    alt value is empty
        CredentialField-->>User: do nothing
    else value is not empty
        CredentialField->>CredentialField: onCopy()
        alt ClipboardAPI available
            CredentialField->>ClipboardAPI: writeText(value)
            alt copy success
                ClipboardAPI-->>CredentialField: ok
                CredentialField->>CredentialField: setStatus(copied)
            else copy failure
                ClipboardAPI--xCredentialField: error
                CredentialField->>CredentialField: console.error(copy failure)
                CredentialField->>CredentialField: setStatus(copyError)
            end
        else ClipboardAPI unavailable
            CredentialField->>CredentialField: throw Error(Clipboard API unavailable)
            CredentialField->>CredentialField: console.error(copy failure)
            CredentialField->>CredentialField: setStatus(copyError)
        end
        CredentialField->>CredentialField: setTimeout(setStatus(idle), 1600ms)
    end
Loading

Class diagram for updated credential field state and behavior

classDiagram
    class CredentialFieldProps {
        string label
        string account
        string placeholder
        boolean mono
        boolean mask
        string defaultValue
    }

    class CredentialFieldState {
        string value
        boolean revealed
        boolean loaded
        boolean dirty
        Status status
        TimeoutHandle debounceRef
    }

    class Status {
        <<enumeration>>
        idle
        saving
        saved
        saveError
        copied
        copyError
    }

    class CredentialFieldComponent {
        +CredentialFieldComponent(props)
        +useEffect_loadCredential(account)
        +useEffect_clearDebounceOnUnmount()
        +save(value)
        +handleChange(event)
        +onBlur()
        +fillDefault()
        +onCopy()
        +render()
    }

    class readCredentialService {
        +readCredential(account) string
    }

    class setCredentialService {
        +setCredential(account, value)
    }

    CredentialFieldComponent --> CredentialFieldProps : uses
    CredentialFieldComponent --> CredentialFieldState : manages
    CredentialFieldState --> Status : has
    CredentialFieldComponent --> readCredentialService : calls
    CredentialFieldComponent --> setCredentialService : calls
Loading

File-Level Changes

Change Details Files
Make credential field state load-aware and prevent writes before initial read completes.
  • Replace saved flag with loaded, dirty, and status state to track lifecycle and user interaction separately.
  • Initialize loaded to false on account change, reset dirty and status, and introduce a cancelled flag in the effect to avoid setting state on unmounted or switched accounts.
  • Treat failed credential reads as errors: log to console and set status to saveError while still marking loaded true so the UI can recover.
  • Disable the input and default-fill button until credentials have finished loading.
openless-all/app/src/pages/Settings.tsx
Harden save behavior so only user-modified values persist and debounce is cleaned up on account switches.
  • Guard save so it is a no-op until loaded is true, and track status as saving/saved/saveError with automatic timeout back to idle.
  • Mark the field dirty on change and default-fill, and clear dirty on successful save so onBlur only triggers saves when the user actually changed the value.
  • Ensure any pending debounce timeout is cleared both on account change and on blur to avoid stale writes after account switching.
openless-all/app/src/pages/Settings.tsx
Improve copy-to-clipboard UX and error handling with an explicit handler and status messaging.
  • Replace inline clipboard call with an onCopy handler that requires a non-empty value and uses navigator.clipboard.writeText when available, otherwise throws.
  • On copy success, set status to copied; on failure, log a console error and set status to copyError, both resetting to idle after a timeout.
  • Render a unified status message span that reflects saving, saved, copied, or error states with appropriate coloring and text, instead of the previous boolean saved indicator.
openless-all/app/src/pages/Settings.tsx

Possibly linked issues

  • #[settings] 凭据字段加载前失焦会清空已有密钥: PR adds loaded/dirty guards and error handling so credential blur/save no longer clears existing stored keys.
  • #[settings] 凭据保存和复制失败没有可见反馈: PR implements visible save/copy status, error handling, and logging for credentials, directly addressing the issue’s requirements.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The new status messages ('保存中', '已复制', '操作失败') are hard-coded; consider routing these through the existing i18n t(...) mechanism for consistency with the rest of the settings UI.
  • Both save and onCopy use window.setTimeout to reset status without any cleanup; consider tracking these timeouts in a ref and clearing them in a useEffect cleanup to avoid possible state updates on an unmounted component.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new status messages ('保存中', '已复制', '操作失败') are hard-coded; consider routing these through the existing i18n `t(...)` mechanism for consistency with the rest of the settings UI.
- Both `save` and `onCopy` use `window.setTimeout` to reset `status` without any cleanup; consider tracking these timeouts in a ref and clearing them in a `useEffect` cleanup to avoid possible state updates on an unmounted component.

## Individual Comments

### Comment 1
<location path="openless-all/app/src/pages/Settings.tsx" line_range="490-495" />
<code_context>
+              whiteSpace: 'nowrap',
+            }}
+          >
+            {status === 'saving'
+              ? '保存中'
+              : status === 'saved'
+                ? t('common.saved')
+                : status === 'copied'
+                  ? '已复制'
+                  : '操作失败'}
+          </span>
</code_context>
<issue_to_address>
**suggestion:** Hardcoded Chinese status messages break consistency with the i18n pattern used elsewhere.

These status texts (`'保存中'`, `'已复制'`, `'操作失败'`) should also use the i18n helper instead of being hardcoded. Please define appropriate keys (e.g. under `settings.credential.*`) and call `t(...)` so status messages are localized consistently with the rest of the component.

Suggested implementation:

```typescript
        {status !== 'idle' && (
          <span
            style={{
              fontSize: 11,
              color: status.endsWith('Error') ? 'var(--ol-warn)' : 'var(--ol-ok)',
              whiteSpace: 'nowrap',
            }}
          >
            {status === 'saving'
              ? t('settings.credential.saving')
              : status === 'saved'
                ? t('common.saved')
                : status === 'copied'
                  ? t('settings.credential.copied')
                  : t('settings.credential.actionFailed')}
          </span>
        )}
      </div>
    </SettingRow>

```

To fully implement this, add the corresponding i18n keys in your locale files, for example:

- In `locales/zh/settings.json` (or equivalent):
  - `"credential.saving": "保存中"`
  - `"credential.copied": "已复制"`
  - `"credential.actionFailed": "操作失败"`

- In other locale files (e.g. `en`), provide appropriate translations for:
  - `settings.credential.saving`
  - `settings.credential.copied`
  - `settings.credential.actionFailed`

Also ensure that the `settings` namespace is loaded for this page if your i18n setup requires explicit namespace loading.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +490 to +495
{status === 'saving'
? '保存中'
: status === 'saved'
? t('common.saved')
: status === 'copied'
? '已复制'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Hardcoded Chinese status messages break consistency with the i18n pattern used elsewhere.

These status texts ('保存中', '已复制', '操作失败') should also use the i18n helper instead of being hardcoded. Please define appropriate keys (e.g. under settings.credential.*) and call t(...) so status messages are localized consistently with the rest of the component.

Suggested implementation:

        {status !== 'idle' && (
          <span
            style={{
              fontSize: 11,
              color: status.endsWith('Error') ? 'var(--ol-warn)' : 'var(--ol-ok)',
              whiteSpace: 'nowrap',
            }}
          >
            {status === 'saving'
              ? t('settings.credential.saving')
              : status === 'saved'
                ? t('common.saved')
                : status === 'copied'
                  ? t('settings.credential.copied')
                  : t('settings.credential.actionFailed')}
          </span>
        )}
      </div>
    </SettingRow>

To fully implement this, add the corresponding i18n keys in your locale files, for example:

  • In locales/zh/settings.json (or equivalent):

    • "credential.saving": "保存中"
    • "credential.copied": "已复制"
    • "credential.actionFailed": "操作失败"
  • In other locale files (e.g. en), provide appropriate translations for:

    • settings.credential.saving
    • settings.credential.copied
    • settings.credential.actionFailed

Also ensure that the settings namespace is loaded for this page if your i18n setup requires explicit namespace loading.

@appergb appergb merged commit bb7300e into Open-Less:main Apr 30, 2026
2 checks passed
appergb pushed a commit that referenced this pull request Apr 30, 2026
包含本轮所有合并:
- Codex 终审两条 HIGH (cancel race) 修复 (PR #79)
- 6 个 Cooper-X-Oak/Codex bot PRs 自动合并 (#44 #49 #53 #68 #72 #73)
- 2 个有冲突 PR 本地 rebase 后合并 (#66 cancel + 空转写并存 / #67 Windows docs)
- README 破图修复 (PR #80)
- workflow-scope 受限的 #48 + #75 由用户在 GitHub UI 直接合并

3 处版本字段同步:package.json + tauri.conf.json + Cargo.toml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants