Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ Selecting a saved profile in the global connection menu opens that exact configu

Scope selection also keeps an application-wide pending state. Escape, the close button and the backdrop can close the dialog, but do not cancel an accepted native selection. Memory editing and further Scope switching stay paused until it finishes. Closing a dialog whose trigger is disabled returns focus to the page heading. A late result still updates authoritative native state, while it cannot clear a new connection editor's draft guard. Failures after the dialog has closed appear in the shared error feedback.

## Existing Server connection journey

Saving, checking and activating a profile are separate actions with distinct progress and completion feedback. Saving does not activate a connection; a completed check asks users to review its individual facts. A compatible activation with a verified identity opens the shared exact-Scope picker. Lookup and selection retain their individual operation restrictions. Select the intended Server-returned Scope, using its complete ID to distinguish duplicate titles; the ID also supplies the button's accessible description. The activation guide then opens Memory and focuses its page heading. Closing the guide restores the activation button without choosing a Scope.

Opening the global Scope picker elsewhere keeps the current page after selection and restores focus to its selector. Cancelling draft confirmation preserves the current input. Confirming a successful Scope switch discards the old Memory or connection-editor draft. Native states rejected as stale cannot open the activation guide or finish Scope navigation. Empty results offer another title, an access request or another connection; loading, permission, authentication and compatibility failures remain separate.

Reopening Desktop loads saved profiles without activating a private session. The local setup entry continues to point to Local Server guidance; approved installer results and default-Scope handoff remain owned with #2117.

## Local Server preparation

Overview remains the initial page. In disconnected Memories, **Set up locally** opens and focuses **Settings & diagnostics → Local Server**; **Connect an existing Server** opens the existing connection editor. Selecting either path does not install software, activate a connection, run diagnostics or probe readiness.
Expand Down
8 changes: 8 additions & 0 deletions desktop/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@

Scope 选择也具有应用层等待状态。Escape、关闭按钮及遮罩可以关闭弹窗,但不会取消已提交的原生选择;完成前暂停记忆编辑及再次切换 Scope。触发按钮被禁用时,关闭弹窗将焦点返回页面标题。晚到结果仍更新原生实际状态,但不会清除新连接编辑器的草稿导航保护;弹窗关闭后发生的失败由共享错误反馈显示。

## 已有 Server 的连接流程

保存配置、检查连接和激活连接是独立操作,分别显示进度与完成反馈。保存不会激活连接;检查完成后须核对各项事实。兼容性与身份验证通过的激活操作会打开共享的精确 Scope 选择器,查找和选择仍分别受逐项操作资格限制。请选择 Server 返回的目标范围,以完整 ID 区分同名 Scope;按钮的无障碍描述也包含该 ID。激活引导完成选择后进入记忆页,并将焦点移至页面标题;关闭引导会将焦点返回激活按钮,不会选择范围。

从其他位置打开全局 Scope 选择器,选择后仍停留在当前页面,焦点返回选择器。取消草稿确认会保留当前输入;确认切换且选择成功后,会丢弃旧记忆草稿或连接编辑草稿。被判为过期的原生状态不能打开激活引导或触发后续 Scope 导航。空结果提供修改标题、申请访问权限或选择其他连接的恢复方向;加载、权限、认证及兼容性错误继续分别呈现。

重新打开 Desktop 只加载已保存配置,不会激活私有会话。本地设置入口继续指向 Local Server 说明;经批准的安装结果与默认 Scope 交接仍需与 #2117 一起完成。

## Local Server 准备说明

默认启动页仍为总览。记忆页未连接时,“本地设置”进入并聚焦“设置与诊断 → Local Server”;“连接已有 Server”进入现有连接编辑器。选择任一路径都不会安装软件、激活连接、执行诊断或探测就绪状态。
Expand Down
34 changes: 34 additions & 0 deletions desktop/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,40 @@ Use a disposable Server and Scope for installed save/search/read/copy checks. Un
those installed workflows. Validate actual Chinese IME, accessibility, Gatekeeper, signing and
notarization independently before distributing outside the internal preview.

## Existing Server connection journey validation

On 2026-10-28, Windows 10 x64 / Python 3.11.15 / Rust 1.95.0 validated the connection
journey working tree based on `aedf7beb1ecff2ea1fade6ccdd8e4bf6a298f799`.
All 130 frontend tests passed, together with TypeScript, lint, generated-contract/icon drift,
formatting and the production build. The English/Chinese first-use scenarios exercise save,
passive check, explicit activation, exact selection among duplicate Scope titles, keyboard
focus and reopening without automatic activation. Other regressions cover cancelled/confirmed
drafts, partial operation support, rejected stale states, late Scope pages and preserving the
current page during global Scope selection. UI tests use jsdom with mocked native IPC.

The native real-Server probe additionally asserts that both saving and passive checking leave
`active` unset before explicit activation and exact Scope selection. The full SQLite harness
passed anonymous loopback, static Bearer, HTTPS with an explicit CA/base path and enforced
Provider, followed by save/search/exact reads, unknown-write handling and identity/revocation
checks. Server source is `aedf7beb1ecff2ea1fade6ccdd8e4bf6a298f799`; wheel
`powercontext-1.4.2.dev36+gaedf7beb1.d20261028-py3-none-any.whl` has SHA-256
`fde108a81a872a6228aad54832f2cd8f899a53bb4a7ff5c321871891fdbc5b68`.
Normalized OpenAPI SHA-256 is
`c12901b88b8e958c9cd63469298b9a04b9d5e2b0b4564d83b50f7ae95542519a`;
debug `server_probe.exe` SHA-256 is
`01bc705b75f7ccb843535b884f7d2a50b7c5d8a5e9d8432b6788dbfafe86a4bf`.
The run used the explicit disposable fixture qualification with a dirty Desktop working tree;
production qualification records are unchanged.

Rust formatting, Clippy and IPC drift checks passed. Seven Python contract/fixture tests passed.
Repository file hooks passed; default Windows `ty check` retains existing POSIX-only attribute
diagnostics in unchanged Python files, while the Linux-target type check passed.
The installed WebDriver workflow now expects activation to open the Scope dialog, exercises
that dialog directly and verifies cleared Memory after selection. That updated installed flow
was not executed locally. Native WebView/credential behavior, IME, screen readers, small windows,
scaling and signed Windows 11/macOS acceptance remain platform qualification work. The approved
local setup/default-Scope handoff remains separate work with #2117; this does not close #2103.

## Independent Ops bridge checks

The native Ops tests exercise streaming with real child processes, malformed/oversized output, deadlines, inherited
Expand Down
9 changes: 9 additions & 0 deletions desktop/src-tauri/examples/server_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ async fn main() {
"authentication":if has_token { "bearer" } else { "unauthenticated_loopback" },
"caPem":fixture.ca_pem,"compatibility":compatibility,"keepCredential":false,"credential":credential
})).unwrap()).unwrap().profiles[0].id.clone();
assert!(manager.state().unwrap().active.is_none());
let checked = manager.check(&profile, false).await.unwrap();
assert!(checked.active.is_none());
assert!(
checked
.reports
.iter()
.any(|report| report.connection_id == profile)
);
let generation = manager.check(&profile, true).await.unwrap().generation;
let generation = manager
.select_scope(generation, &fixture.scope_id)
Expand Down
9 changes: 5 additions & 4 deletions desktop/tests/installed_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def activate(self, name: str) -> None:
self.element(
f"//ul[@class='profile-list']/li[.//span[@class='profile-name'][normalize-space(.)='{name}']]//span[contains(@class,'badge')]"
)
self.button("记忆")
self.element("//div[@role='dialog' and @aria-label='选择精确范围']")

def connect(self, name: str, endpoint: str) -> None:
self.button("连接")
Expand All @@ -144,7 +144,8 @@ def connect(self, name: str, endpoint: str) -> None:
self.activate(name)

def select_scope(self, scope_id: str) -> None:
self.click("//div[@class='topbar-group']/button")
if not self.observe("return !!document.querySelector('[role=dialog]');"):
self.click("//div[@class='topbar-group']/button")
self.type("按范围标题查找", "Desktop installed CI")
self.button("查找范围")
self.click(f"//ul[@class='scope-list']/li[code[normalize-space(.)='{scope_id}']]/button")
Expand Down Expand Up @@ -281,8 +282,8 @@ def exercise_connection_isolation(
seeded.raise_for_status()
reference_b = saved_reference(seeded.json())
page.connect("Desktop CI B", str(server_b.base_url).rstrip("/"))
page.expect_empty_context()
page.select_scope(scope_b)
page.expect_empty_context()
if page.search_read(text_b) != reference_b:
raise HarnessFailure("installed_second_server_reference_mismatch")
draft = "desktopunsavedci 不应写入的草稿"
Expand All @@ -302,8 +303,8 @@ def exercise_connection_isolation(
page.button("连接")
page.profile("Desktop CI synthetic")
page.activate("Desktop CI synthetic")
page.expect_empty_context()
page.select_scope(scope_a)
page.expect_empty_context()
if page.search_read(NOTE) != current_reference_a:
raise HarnessFailure("installed_reconnected_reference_mismatch")
page.button("连接")
Expand Down
25 changes: 21 additions & 4 deletions desktop/ui/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ export function App() {
const [menu, setMenu] = useState(false);
const [host, setHost] = useState<FoundationInfo | null>(null);
const [desktop, setDesktop] = useState<DesktopState | null>(null);
const currentState = useRef<DesktopState | null>(null);
const [nativeError, setNativeError] = useState<unknown>(null);
const [stateError, setStateError] = useState<unknown>(null);
const [stateLoading, setStateLoading] = useState(true);
const stateRequest = useRef(0);
const [dirty, setDirty] = useState(false);
const [connectionDraftReset, setConnectionDraftReset] = useState(0);
const [scopeOpen, setScopeOpen] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [connectionSelection, setConnectionSelection] = useState<{
Expand All @@ -74,9 +76,14 @@ export function App() {
setPendingConnections((current) => current + (pending ? 1 : -1));
}
function receiveState(next: DesktopState) {
setDesktop((current) =>
!current || next.generation >= current.generation ? next : current,
);
if (
currentState.current &&
next.generation < currentState.current.generation
)
return false;
currentState.current = next;
setDesktop(next);
return true;
}
const { confirm, confirmation } = useConfirmation(language);
function confirmSwitch() {
Expand All @@ -90,6 +97,7 @@ export function App() {
const localServerHeading = useRef<HTMLHeadingElement>(null);
const settingsSection = useRef<"local-server" | undefined>(undefined);
const scopeReturnFocus = useRef<HTMLElement | null>(null);
const scopeToMemory = useRef(false);
const t = messages[language];
const ct = connectionMessages[language];
async function loadState() {
Expand Down Expand Up @@ -123,8 +131,9 @@ export function App() {
target?.focus();
if (localServer) target?.scrollIntoView?.({ block: "start" });
}, [page, connectionSelection]);
function openScopes(trigger: HTMLElement) {
function openScopes(trigger: HTMLElement, toMemory = false) {
scopeReturnFocus.current = trigger;
scopeToMemory.current = toMemory;
setScopeOpen(true);
}
async function navigate(
Expand Down Expand Up @@ -341,6 +350,8 @@ export function App() {
onState={receiveState}
onDirty={setDirty}
onPendingChange={connectionPending}
onActivated={(trigger) => openScopes(trigger, true)}
discardDraft={connectionDraftReset}
/>
)}
{page === "observation" && (
Expand Down Expand Up @@ -423,8 +434,14 @@ export function App() {
onPendingChange={scopePending}
onSelectionError={setNativeError}
onSelected={() => {
if (page === "connections" && dirty)
setConnectionDraftReset((current) => current + 1);
setDirty(false);
setScopeOpen(false);
if (scopeToMemory.current) {
scopeReturnFocus.current = null;
setPage("memories");
}
}}
hideHeading
onManageConnections={() => {
Expand Down
Loading
Loading