dsh-webdav-sync 1.0.2 → 1.0.4

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/lib/client.js CHANGED
@@ -82,7 +82,7 @@ window.__ModuleLoader__.load({
82
82
  }
83
83
  //#endregion
84
84
  //#region \0webdav-sync-css:/home/runner/work/dsh-webdav/dsh-webdav/src/client/WebdavSyncCard.module.css?inline
85
- const css = ".summary_318697645 {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.summaryText_318697645 {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.page_318697645 {\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 4px 0 8px;\n font-size: 13px;\n color: var(--dsw-alias-label-primary);\n}\n\n.legend_318697645 {\n display: flex;\n align-items: baseline;\n gap: 8px;\n margin: 0 0 8px;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.hint_318697645 {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* The two stamps share one line: the check on the left, the sync on the right.\n They wrap rather than overlap when the card is too narrow to hold both. */\n.times_318697645 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 4px 10px;\n flex-wrap: wrap;\n}\n\n.field_318697645 {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-bottom: 10px;\n}\n\n.fieldLabel_318697645 {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/*\n * The line of explanation belonging to one field: it sits between that field's\n * label and its input, so a reader gets the rule before typing, and the gap\n * between two fields (10px) still reads as the bigger break.\n */\n.fieldHint_318697645 {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.grid_318697645 {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n gap: 10px 14px;\n}\n\n/*\n * The extra-headers field is the card's only multi-line input, and there is no\n * multi-line primitive to reuse. It takes the one-line input's frame instead —\n * same border, radius and background — so it reads as a field of the same\n * family, and a monospace face because what goes in it is header syntax, where\n * the alignment of a name against its value is the thing being checked. It\n * starts three lines tall and may be dragged taller.\n */\n.headersArea_318697645 {\n box-sizing: border-box;\n width: 100%;\n min-height: 66px;\n padding: 6px 8px;\n resize: vertical;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-1);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n\n.headersArea_318697645::placeholder {\n color: var(--dsw-alias-label-dimmed);\n}\n\n.headersArea_318697645:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n/*\n * A field's label line when the field also carries an option of its own: the\n * label and the saved-password checkbox share the line, so the option reads as\n * part of the field it governs instead of as a setting of its own further down\n * the card. They sit at the two ends of that line (`space-between`): the label\n * keeps the field column's left edge, the checkbox lines up with its right edge\n * — packed together they read as one long word. The checkbox is brought down to\n * the field label's 12px and its colour for the same reason as the part rows\n * (see `.partRow_318697645 label`): a control inside a label line should not outrank the\n * line it sits in.\n */\n.labelRow_318697645 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n}\n\n.labelRow_318697645 label {\n gap: 6px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.labelRow_318697645 label input {\n width: 14px;\n height: 14px;\n}\n\n/*\n * The auto-sync choice: every tab in one row, all the same width, so six\n * options read as one strip instead of a dropdown hiding five of them.\n */\n/* Doubled class: the control's own rule also sets a display, and this wins. */\n.autoControl_318697645.autoControl_318697645 {\n display: grid;\n width: 100%;\n box-sizing: border-box;\n}\n\n/* Six tabs have to share the row, so each keeps its padding modest. */\n.autoControl_318697645 button {\n padding: 0 6px;\n font-size: 12px;\n}\n\n.partRow_318697645 {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 7px 0;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n\n/* No trailing border when the rows stand alone (a host with no view yet). */\n.partRow_318697645:last-child {\n border-bottom: none;\n}\n\n/* The checkbox with its one-line description under it. */\n.partText_318697645 {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n/*\n * The checkbox component sets its own label at 14px, which is bigger than this\n * card's section headings (12px) — the rows read as if they outranked the\n * heading above them. Its class name is internal to the component, so the\n * element selector is the way in; the box shrinks with the text to match.\n */\n.partRow_318697645 label {\n gap: 6px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.partRow_318697645 label input {\n width: 14px;\n height: 14px;\n}\n\n.partHint_318697645 {\n font-size: 11px;\n line-height: 1.4;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.partState_318697645 {\n margin-left: auto;\n flex: none;\n padding-top: 2px;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.partStateBoth_318697645 {\n color: var(--dsw-alias-label-warning, var(--dsw-alias-label-secondary));\n}\n\n/*\n * The extra directories, laid out as one more part row — checkbox, name,\n * one-line description, state — exactly like the four above, with the picker on\n * the title line right after the name and the chosen paths listed under the row.\n * Each path is a small card of its own: a bordered row that reads as something\n * carried, not as another line of prose.\n */\n\n/*\n * The picker shares the title line with the switch. It is a text button, so it\n * has to stop borrowing a control's height and padding to sit level with a 12px\n * label — and it reads quieter than the label until it is pointed at.\n */\n.titleRow_318697645 {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.titleRow_318697645 button {\n height: auto;\n min-height: 0;\n padding: 0 2px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.titleRow_318697645 button:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n\n.dirList_318697645 {\n padding: 0 0 4px;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n\n/* The group owns the separator; its heading row sits above the paths. */\n.dirList_318697645 .partRow_318697645 {\n border-bottom: none;\n align-items: center;\n}\n\n.dirRow_318697645 {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0 0 4px 20px;\n padding: 4px 6px 4px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n}\n\n/* Switched off, the paths stay on screen but read as dormant. */\n.dirOff_318697645 {\n opacity: 0.6;\n}\n\n/* Mono, because a path is read character by character; a narrow box drops the\n * tail, and the whole path stays in the row's title attribute. */\n.dirPath_318697645 {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-primary);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* The row a path is typed into, when no dialog could be reached. */\n.dirRow_318697645 input {\n flex: 1;\n min-width: 0;\n}\n\n/* A configured directory this computer could not read, one line per directory. */\n.problem_318697645 {\n margin: 4px 0 0;\n font-size: 11px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n\n.notice_318697645 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2);\n font-size: 12px;\n}\n\n.noticeTitle_318697645 {\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n/*\n * The sync actions. Six short labels have to sit on one row, so the buttons\n * keep their own padding down; wrapping stays on only as a narrow-card escape.\n * With no frame to separate them, the spacing has to: each label carries 6px of\n * its own and the row adds 2px, so neighbouring labels sit 14px apart — roomy\n * enough to read as separate actions, tight enough not to break the row.\n */\n.footer_318697645 {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-wrap: wrap;\n padding-top: 4px;\n}\n\n/*\n * Every sync action is a text button: no frame, no fill, only a label. The\n * reason is the row itself — six framed buttons next to the settings above read\n * as a second form — and the risk a text button always carries: 「看不出是个\n * 按钮」. That is answered by colour and by the pointer, not by a box:\n *\n * - the label sits in the ordinary primary colour, so it reads as text that\n * happens to be clickable;\n * - the hover fill comes from `ghost` and appears under the pointer, which is\n * exactly when the question 「能点吗」 gets asked;\n * - the focus ring is drawn here because `ghost` has none of its own, and a\n * keyboard user has no pointer to discover the button with.\n *\n * `height: auto` drops the control's 28px box so six labels fit one line; the\n * 24px `min-height` keeps the hover fill from collapsing onto the glyphs. The\n * 500 weight is what makes the whole row read as one band of controls rather\n * than as a sentence that happens to end the section: at 12px, ordinary body\n * weight next to ordinary body text is the one thing a text button cannot\n * recover from with colour alone.\n */\n.footer_318697645 button.textAction_318697645 {\n height: auto;\n min-height: 24px;\n padding: 0 6px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.footer_318697645 button.textAction_318697645:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.footer_318697645 button.textAction_318697645:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n\n/*\n * The one action this part of the card is for. It is the row's only accented\n * label, which is what makes 「立即同步」 findable without a filled box; the\n * colour carries it in both themes, where a 12px label is small enough that\n * hue alone would need a second signal — here that signal is being the only\n * coloured one in an otherwise grey row, so the weight stays the row's own.\n */\n.footer_318697645 button.textActionPrimary_318697645 {\n color: var(--dsw-alias-state-business-primary);\n}\n\n.footer_318697645 button.textActionPrimary_318697645:hover:not(:disabled) {\n color: var(--dsw-alias-state-business-primary);\n}\n\n/*\n * The two actions that replace one whole side with the other: 「服务器覆盖本机」\n * and 「本机覆盖服务器」. They are the only pair in the row that can throw away\n * work on the side it writes, so they carry the warning colour while staying\n * text buttons. Not the error colour: both are reversible (a restore has\n * `undo`, an upload keeps the old file in `sync/`), and red would say otherwise.\n *\n * `warn-label`, not `warn-primary`: the design system splits these two — the\n * primary amber (#f59e0b) paints dots and tag fills, while warning *text* uses\n * the darker `label` step (#dd8629), which is what `Toast` and\n * `ConnectionIndicator` put on 12px labels exactly like this one. At 12px the\n * primary amber on a light card is barely 2:1 against the surface; the label\n * step is the token that was picked for reading, so it is the one used here.\n */\n.footer_318697645 button.textActionReplace_318697645 {\n color: var(--dsw-alias-state-warn-label);\n}\n\n.footer_318697645 button.textActionReplace_318697645:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-state-warn-label) 10%, transparent);\n color: var(--dsw-alias-state-warn-label);\n}\n\n.spacer_318697645 {\n flex: 1;\n}\n\n/* The card's own actions: WebDAV settings, kept apart from the sync ones. */\n.cardFooter_318697645 {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n margin-top: 4px;\n padding-top: 12px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n\n.failed_318697645 {\n font-size: 12px;\n color: var(--dsw-alias-label-error);\n}\n\n.note_318697645 {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.confirm_318697645 {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l4);\n border-radius: 10px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n\n.previewList_318697645 {\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 12px;\n}\n\n.previewRow_318697645 {\n display: flex;\n gap: 8px;\n}\n\n.previewPart_318697645 {\n min-width: 90px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.output_318697645 {\n max-height: 180px;\n margin: 0;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n line-height: 1.5;\n white-space: pre-wrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n.spin_318697645 {\n animation: webdavSyncSpin 0.9s linear infinite;\n}\n\n@keyframes webdavSyncSpin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/*\n * The forwarder's switch and the two values it hands out.\n *\n * The switch is a checkbox like the ones that choose the parts, and it reads at\n * the size of the section titles rather than at the primitive's own 14px: one\n * line of body-sized text under a title is a control shouting over its heading.\n *\n * The address is what a reader copies, so it is plain text — monospace, because\n * a path is read character by character — with the copy icon right after it and\n * no frame: a field around a value nobody types only reads as something to fill\n * in. The account sits under it the same way: name, value, copy icon. All three\n * are named, because three copy buttons in a row are otherwise three buttons\n * that all say 「copy」.\n */\n.relaySwitch_318697645 {\n margin-bottom: 6px;\n}\n\n.relaySwitch_318697645 label {\n font-size: 12px;\n line-height: 18px;\n}\n\n.relayAddressRow_318697645,\n.relayAccountRow_318697645 {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n margin-bottom: 6px;\n}\n\n/* The two account values are two groups rather than four loose items: the wider\n * gap separates them, the narrow one binds each label to its value and icon. */\n.relayAccountRow_318697645 {\n gap: 6px 14px;\n}\n\n/* One value with its copy icon. It stays on one line, so a wrapped card can\n * never leave an icon under a value it does not copy. */\n.relayPair_318697645 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n/* Plain text rather than a field frame, but still the reader's to select: the\n * failure hint's fallback — select it and copy by hand — has to keep working.\n * It takes only the width it needs, so the copy icon sits at the end of the\n * address instead of being pushed out to the card's right edge: the icon\n * belongs to that string, and a gap would read as a separate action. */\n.relayAddress_318697645 {\n flex: 0 1 auto;\n min-width: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n overflow-wrap: anywhere;\n}\n\n/* An icon button: the primitive's frame drops away, and what is left is the\n * app's own pressable shape — the hover wash around a 16px glyph, at the 24px\n * the app's other icon actions take. */\nbutton.relayCopy_318697645 {\n flex: none;\n width: 24px;\n height: 24px;\n padding: 0;\n color: var(--dsw-alias-label-secondary);\n}\n\nbutton.relayCopy_318697645:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n\n.relayLabel_318697645 {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* A value with nothing around it: the three things a reader takes away — the\n * address and the two account strings — are all set the same way, because none\n * of them is a field. A frame here would say 「fill this in」 about a string that\n * is generated and read-only. */\n.relayValue_318697645 {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n";
85
+ const css = ".summary_318697645 {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.summaryText_318697645 {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.page_318697645 {\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 4px 0 8px;\n font-size: 13px;\n color: var(--dsw-alias-label-primary);\n}\n\n.legend_318697645 {\n display: flex;\n align-items: baseline;\n gap: 8px;\n margin: 0 0 8px;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.hint_318697645 {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* The two stamps share one line: the check on the left, the sync on the right.\n They wrap rather than overlap when the card is too narrow to hold both. */\n.times_318697645 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 4px 10px;\n flex-wrap: wrap;\n}\n\n.field_318697645 {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-bottom: 10px;\n}\n\n.fieldLabel_318697645 {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/*\n * The line of explanation belonging to one field: it sits between that field's\n * label and its input, so a reader gets the rule before typing, and the gap\n * between two fields (10px) still reads as the bigger break.\n */\n.fieldHint_318697645 {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.grid_318697645 {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n gap: 10px 14px;\n}\n\n/*\n * The extra-headers field is the card's only multi-line input, and there is no\n * multi-line primitive to reuse. It takes the one-line input's frame instead —\n * same border, radius and background — so it reads as a field of the same\n * family, and a monospace face because what goes in it is header syntax, where\n * the alignment of a name against its value is the thing being checked. It\n * starts three lines tall and may be dragged taller.\n */\n.headersArea_318697645 {\n box-sizing: border-box;\n width: 100%;\n min-height: 66px;\n padding: 6px 8px;\n resize: vertical;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-1);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n\n.headersArea_318697645::placeholder {\n color: var(--dsw-alias-label-dimmed);\n}\n\n.headersArea_318697645:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n/*\n * A field's label line when the field also carries an option of its own: the\n * label and the saved-password checkbox share the line, so the option reads as\n * part of the field it governs instead of as a setting of its own further down\n * the card. They sit at the two ends of that line (`space-between`): the label\n * keeps the field column's left edge, the checkbox lines up with its right edge\n * — packed together they read as one long word. The checkbox is brought down to\n * the field label's 12px and its colour for the same reason as the part rows\n * (see `.partRow_318697645 label`): a control inside a label line should not outrank the\n * line it sits in.\n */\n.labelRow_318697645 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n}\n\n.labelRow_318697645 label {\n gap: 6px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.labelRow_318697645 label input {\n width: 14px;\n height: 14px;\n}\n\n/*\n * The auto-sync choice: every tab in one row, all the same width, so six\n * options read as one strip instead of a dropdown hiding five of them.\n */\n/* Doubled class: the control's own rule also sets a display, and this wins. */\n.autoControl_318697645.autoControl_318697645 {\n display: grid;\n width: 100%;\n box-sizing: border-box;\n}\n\n/* Six tabs have to share the row, so each keeps its padding modest. */\n.autoControl_318697645 button {\n padding: 0 6px;\n font-size: 12px;\n}\n\n.partRow_318697645 {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 7px 0;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n\n/* No trailing border when the rows stand alone (a host with no view yet). */\n.partRow_318697645:last-child {\n border-bottom: none;\n}\n\n/* The checkbox with its one-line description under it. */\n.partText_318697645 {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n/*\n * The checkbox component sets its own label at 14px, which is bigger than this\n * card's section headings (12px) — the rows read as if they outranked the\n * heading above them. Its class name is internal to the component, so the\n * element selector is the way in; the box shrinks with the text to match.\n */\n.partRow_318697645 label {\n gap: 6px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.partRow_318697645 label input {\n width: 14px;\n height: 14px;\n}\n\n.partHint_318697645 {\n font-size: 11px;\n line-height: 1.4;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.partState_318697645 {\n margin-left: auto;\n flex: none;\n padding-top: 2px;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.partStateBoth_318697645 {\n color: var(--dsw-alias-label-warning, var(--dsw-alias-label-secondary));\n}\n\n/*\n * The extra directories, laid out as one more part row — checkbox, name,\n * one-line description, state — exactly like the four above, with the picker on\n * the title line right after the name and the chosen paths listed under the row.\n * Each path is a small card of its own: a bordered row that reads as something\n * carried, not as another line of prose.\n */\n\n/*\n * The picker shares the title line with the switch. It is a text button, so it\n * has to stop borrowing a control's height and padding to sit level with a 12px\n * label — and it reads quieter than the label until it is pointed at.\n */\n.titleRow_318697645 {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.titleRow_318697645 button {\n height: auto;\n min-height: 0;\n padding: 0 2px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.titleRow_318697645 button:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n\n.dirList_318697645 {\n padding: 0 0 4px;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n\n/* The group owns the separator; its heading row sits above the paths. */\n.dirList_318697645 .partRow_318697645 {\n border-bottom: none;\n align-items: center;\n}\n\n.dirRow_318697645 {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0 0 4px 20px;\n padding: 4px 6px 4px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n}\n\n/* Switched off, the paths stay on screen but read as dormant. */\n.dirOff_318697645 {\n opacity: 0.6;\n}\n\n/* Mono, because a path is read character by character; a narrow box drops the\n * tail, and the whole path stays in the row's title attribute. */\n.dirPath_318697645 {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-primary);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* The row a path is typed into, when no dialog could be reached. */\n.dirRow_318697645 input {\n flex: 1;\n min-width: 0;\n}\n\n/* A configured directory this computer could not read, one line per directory. */\n.problem_318697645 {\n margin: 4px 0 0;\n font-size: 11px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n\n.notice_318697645 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2);\n font-size: 12px;\n}\n\n.noticeTitle_318697645 {\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n/*\n * The sync actions. Six short labels have to sit on one row, so the buttons\n * keep their own padding down; wrapping stays on only as a narrow-card escape.\n * With no frame to separate them, the spacing has to: each label carries 6px of\n * its own and the row adds 2px, so neighbouring labels sit 14px apart — roomy\n * enough to read as separate actions, tight enough not to break the row.\n */\n.footer_318697645 {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-wrap: wrap;\n padding-top: 4px;\n}\n\n/*\n * Every sync action is a text button: no frame, no fill, only a label. The\n * reason is the row itself — six framed buttons next to the settings above read\n * as a second form — and the risk a text button always carries: 「看不出是个\n * 按钮」. That is answered by colour and by the pointer, not by a box:\n *\n * - the label sits in the ordinary primary colour, so it reads as text that\n * happens to be clickable;\n * - the hover fill comes from `ghost` and appears under the pointer, which is\n * exactly when the question 「能点吗」 gets asked;\n * - the focus ring is drawn here because `ghost` has none of its own, and a\n * keyboard user has no pointer to discover the button with.\n *\n * `height: auto` drops the control's 28px box so six labels fit one line; the\n * 24px `min-height` keeps the hover fill from collapsing onto the glyphs. The\n * 500 weight is what makes the whole row read as one band of controls rather\n * than as a sentence that happens to end the section: at 12px, ordinary body\n * weight next to ordinary body text is the one thing a text button cannot\n * recover from with colour alone.\n */\n.footer_318697645 button.textAction_318697645 {\n height: auto;\n min-height: 24px;\n padding: 0 6px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.footer_318697645 button.textAction_318697645:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.footer_318697645 button.textAction_318697645:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n\n/*\n * The one action this part of the card is for. It is the row's only accented\n * label, which is what makes 「立即同步」 findable without a filled box; the\n * colour carries it in both themes, where a 12px label is small enough that\n * hue alone would need a second signal — here that signal is being the only\n * coloured one in an otherwise grey row, so the weight stays the row's own.\n */\n.footer_318697645 button.textActionPrimary_318697645 {\n color: var(--dsw-alias-state-business-primary);\n}\n\n.footer_318697645 button.textActionPrimary_318697645:hover:not(:disabled) {\n color: var(--dsw-alias-state-business-primary);\n}\n\n/*\n * The two actions that replace one whole side with the other: 「服务器覆盖本机」\n * and 「本机覆盖服务器」. They are the only pair in the row that can throw away\n * work on the side it writes, so they carry the warning colour while staying\n * text buttons. Not the error colour: both are reversible (a restore has\n * `undo`, an upload keeps the old file in `sync/`), and red would say otherwise.\n *\n * `warn-label`, not `warn-primary`: the design system splits these two — the\n * primary amber (#f59e0b) paints dots and tag fills, while warning *text* uses\n * the darker `label` step (#dd8629), which is what `Toast` and\n * `ConnectionIndicator` put on 12px labels exactly like this one. At 12px the\n * primary amber on a light card is barely 2:1 against the surface; the label\n * step is the token that was picked for reading, so it is the one used here.\n */\n.footer_318697645 button.textActionReplace_318697645 {\n color: var(--dsw-alias-state-warn-label);\n}\n\n.footer_318697645 button.textActionReplace_318697645:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-state-warn-label) 10%, transparent);\n color: var(--dsw-alias-state-warn-label);\n}\n\n.spacer_318697645 {\n flex: 1;\n}\n\n/* The card's own actions: WebDAV settings, kept apart from the sync ones. */\n.cardFooter_318697645 {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n margin-top: 4px;\n padding-top: 12px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n\n.failed_318697645 {\n font-size: 12px;\n color: var(--dsw-alias-label-error);\n}\n\n.note_318697645 {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.confirm_318697645 {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l4);\n border-radius: 10px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n\n.previewList_318697645 {\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 12px;\n}\n\n.previewRow_318697645 {\n display: flex;\n gap: 8px;\n}\n\n.previewPart_318697645 {\n min-width: 90px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.output_318697645 {\n max-height: 180px;\n margin: 0;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n line-height: 1.5;\n white-space: pre-wrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/*\n * The install report: how it went, the log, and the way out.\n *\n * The button sits on the sentence's row rather than under the log, so closing it\n * is where the eye already is — and one button takes the whole report away, since\n * the log is there to be read once and then is only something to scroll past.\n */\n.outputBlock_318697645 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.outputHead_318697645 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.spin_318697645 {\n animation: webdavSyncSpin 0.9s linear infinite;\n}\n\n@keyframes webdavSyncSpin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/*\n * The forwarder's switch and the two values it hands out.\n *\n * The switch is a checkbox like the ones that choose the parts, and it reads at\n * the size of the section titles rather than at the primitive's own 14px: one\n * line of body-sized text under a title is a control shouting over its heading.\n *\n * The address is what a reader copies, so it is plain text — monospace, because\n * a path is read character by character — with the copy icon right after it and\n * no frame: a field around a value nobody types only reads as something to fill\n * in. The account sits under it the same way: name, value, copy icon. All three\n * are named, because three copy buttons in a row are otherwise three buttons\n * that all say 「copy」.\n */\n.relaySwitch_318697645 {\n margin-bottom: 6px;\n}\n\n.relaySwitch_318697645 label {\n font-size: 12px;\n line-height: 18px;\n}\n\n.relayAddressRow_318697645,\n.relayAccountRow_318697645 {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n margin-bottom: 6px;\n}\n\n/* The two account values are two groups rather than four loose items: the wider\n * gap separates them, the narrow one binds each label to its value and icon. */\n.relayAccountRow_318697645 {\n gap: 6px 14px;\n}\n\n/* One value with its copy icon. It stays on one line, so a wrapped card can\n * never leave an icon under a value it does not copy. */\n.relayPair_318697645 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n/* Plain text rather than a field frame, but still the reader's to select: the\n * failure hint's fallback — select it and copy by hand — has to keep working.\n * It takes only the width it needs, so the copy icon sits at the end of the\n * address instead of being pushed out to the card's right edge: the icon\n * belongs to that string, and a gap would read as a separate action. */\n.relayAddress_318697645 {\n flex: 0 1 auto;\n min-width: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n overflow-wrap: anywhere;\n}\n\n/* An icon button: the primitive's frame drops away, and what is left is the\n * app's own pressable shape — the hover wash around a 16px glyph, at the 24px\n * the app's other icon actions take. */\nbutton.relayCopy_318697645 {\n flex: none;\n width: 24px;\n height: 24px;\n padding: 0;\n color: var(--dsw-alias-label-secondary);\n}\n\nbutton.relayCopy_318697645:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n\n.relayLabel_318697645 {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* A value with nothing around it: the three things a reader takes away — the\n * address and the two account strings — are all set the same way, because none\n * of them is a field. A frame here would say 「fill this in」 about a string that\n * is generated and read-only. */\n.relayValue_318697645 {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n";
86
86
  const tagId = "dsh-webdav-sync/WebdavSyncCard.module.css";
87
87
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
88
88
  const tag = document.createElement("style");
@@ -131,6 +131,8 @@ window.__ModuleLoader__.load({
131
131
  "previewRow": "previewRow_318697645",
132
132
  "previewPart": "previewPart_318697645",
133
133
  "output": "output_318697645",
134
+ "outputBlock": "outputBlock_318697645",
135
+ "outputHead": "outputHead_318697645",
134
136
  "spin": "spin_318697645",
135
137
  "relaySwitch": "relaySwitch_318697645",
136
138
  "relayAddressRow": "relayAddressRow_318697645",
@@ -332,24 +334,24 @@ window.__ModuleLoader__.load({
332
334
  /** Which dictionary key names each part. */
333
335
  const PART_NAME = {
334
336
  models: "partModels",
335
- defaults: "partDefaults",
336
337
  plugins: "partPlugins",
337
338
  patch: "partPatch",
338
- dirs: "partDirs"
339
+ dirs: "partDirs",
340
+ keys: "partKeys"
339
341
  };
340
342
  /** Which dictionary key says, in a line, what a part carries. */
341
343
  const PART_HINT = {
342
344
  models: "partModelsHint",
343
- defaults: "partDefaultsHint",
344
345
  plugins: "partPluginsHint",
345
346
  patch: "partPatchHint",
346
- dirs: "partDirsHint"
347
+ dirs: "partDirsHint",
348
+ keys: "partKeysHint"
347
349
  };
348
350
  /**
349
351
  * The parts whose switch is a checkbox on their own row.
350
352
  *
351
353
  * The extra directories are a `part` too — they hash, merge and travel like the
352
- * four profile parts — but there is no single boolean behind them: the list
354
+ * profile parts — but there is no single boolean behind them: the list
353
355
  * itself is the part, so it is turned off by removing every row, and its rows
354
356
  * are edited one by one instead of behind one label. Filtering it out keeps the
355
357
  * checkbox loop honest, and the predicate is what tells the compiler so.
@@ -386,6 +388,20 @@ window.__ModuleLoader__.load({
386
388
  return "partSynced";
387
389
  }
388
390
  /**
391
+ * The state word of one part's row, asked with the draft the row is bound to.
392
+ *
393
+ * The box and this word sit in the same row and have to answer for the same
394
+ * thing, so both read the draft. The host's `synced` says what the configuration
395
+ * on disk carries, and `localChanged` / `remoteChanged` describe that same saved
396
+ * configuration; while the draft disagrees with it, no word of the four is true —
397
+ * "in step" and "off" both speak about a save that has not happened yet. The row
398
+ * then says the one thing that is the case: the change is waiting for the save.
399
+ */
400
+ function rowStateKey(draft, view) {
401
+ if (draft !== view.synced) return "partUnsaved";
402
+ return draft ? partStateKey(view) : "partOff";
403
+ }
404
+ /**
389
405
  * The last sync time as this computer's own clock reads it: the host stores an
390
406
  * ISO string in UTC, which is not what a person recognises at a glance.
391
407
  */
@@ -590,13 +606,14 @@ window.__ModuleLoader__.load({
590
606
  children: t("partsLegend")
591
607
  }),
592
608
  (state.view?.parts ?? []).filter(isToggledPart).map((part) => {
593
- const stateKey = part.synced ? partStateKey(part) : "partOff";
609
+ const on = state[part.part];
610
+ const stateKey = rowStateKey(on, part);
594
611
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
595
612
  className: WebdavSyncCard_module_default.partRow,
596
613
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
597
614
  className: WebdavSyncCard_module_default.partText,
598
615
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
599
- checked: part.synced,
616
+ checked: on,
600
617
  label: t(PART_NAME[part.part]),
601
618
  title: t("partOffHint"),
602
619
  onChange: (next) => props.toggle(part.part, next)
@@ -636,7 +653,7 @@ window.__ModuleLoader__.load({
636
653
  })]
637
654
  }), dirsPart !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
638
655
  className: WebdavSyncCard_module_default.partState,
639
- children: t(dirsPart.synced ? partStateKey(dirsPart) : "partOff")
656
+ children: t(rowStateKey(state.dirsEnabled, dirsPart))
640
657
  })]
641
658
  }), state.dirs.map((path, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
642
659
  className: state.dirsEnabled ? WebdavSyncCard_module_default.dirRow : `${WebdavSyncCard_module_default.dirRow} ${WebdavSyncCard_module_default.dirOff}`,
@@ -669,6 +686,22 @@ window.__ModuleLoader__.load({
669
686
  message: problem.message
670
687
  })
671
688
  }, problem.path)),
689
+ (state.view?.keysProblems ?? []).map((problem) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
690
+ className: WebdavSyncCard_module_default.problem,
691
+ role: "status",
692
+ children: problem.ref === "" ? t("keyStoreProblem", { message: problem.message }) : t("keyProblem", {
693
+ ref: problem.ref,
694
+ message: problem.message
695
+ })
696
+ }, problem.ref)),
697
+ state.keysProblems.map((problem) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
698
+ className: WebdavSyncCard_module_default.problem,
699
+ role: "status",
700
+ children: problem.ref === "" ? t("keyStoreProblem", { message: problem.message }) : t("keyProblem", {
701
+ ref: problem.ref,
702
+ message: problem.message
703
+ })
704
+ }, `last-${problem.ref}`)),
672
705
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
673
706
  className: WebdavSyncCard_module_default.times,
674
707
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -772,13 +805,24 @@ window.__ModuleLoader__.load({
772
805
  })
773
806
  ]
774
807
  }),
775
- state.installOutput !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
776
- className: state.messageFailed ? WebdavSyncCard_module_default.failed : WebdavSyncCard_module_default.note,
777
- children: state.messageFailed ? t("installFailed") : t("installOk")
778
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
779
- className: WebdavSyncCard_module_default.output,
780
- children: state.installOutput
781
- })] }),
808
+ state.installOutput !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
809
+ className: WebdavSyncCard_module_default.outputBlock,
810
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
811
+ className: WebdavSyncCard_module_default.outputHead,
812
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
813
+ className: state.installFailed ? WebdavSyncCard_module_default.failed : WebdavSyncCard_module_default.note,
814
+ children: state.installFailed ? t("installFailed") : t("installOk")
815
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
816
+ variant: "outline",
817
+ size: "sm",
818
+ onClick: () => props.closeOutput(),
819
+ children: t("outputClose")
820
+ })]
821
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
822
+ className: WebdavSyncCard_module_default.output,
823
+ children: state.installOutput
824
+ })]
825
+ }),
782
826
  state.preview !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
783
827
  className: WebdavSyncCard_module_default.legend,
784
828
  children: t("previewTitle")
@@ -977,6 +1021,17 @@ window.__ModuleLoader__.load({
977
1021
  }
978
1022
  //#endregion
979
1023
  //#region src/client/card-store.ts
1024
+ /**
1025
+ * The keys an action could not write, as the host reported them.
1026
+ *
1027
+ * Every action that applies a merge answers with the same field, and an absent one
1028
+ * means every key went in — so this reads both shapes as "no problems" rather than
1029
+ * making each caller remember which.
1030
+ */
1031
+ function keyProblemsOf(result) {
1032
+ const held = result.keysProblems;
1033
+ return Array.isArray(held) ? held : [];
1034
+ }
980
1035
  const EMPTY = {
981
1036
  phase: "loading",
982
1037
  error: "",
@@ -987,13 +1042,13 @@ window.__ModuleLoader__.load({
987
1042
  passphrase: "",
988
1043
  headers: "",
989
1044
  dirs: [],
990
- dirsEnabled: true,
1045
+ dirsEnabled: false,
991
1046
  rememberPassword: true,
992
1047
  autoEvery: -1,
993
1048
  models: true,
994
- defaults: true,
995
1049
  plugins: true,
996
1050
  patch: true,
1051
+ keys: true,
997
1052
  relayEnabled: false,
998
1053
  relayPort: RELAY_DEFAULT_PORT,
999
1054
  dirty: false,
@@ -1002,6 +1057,8 @@ window.__ModuleLoader__.load({
1002
1057
  messageFailed: false,
1003
1058
  preview: void 0,
1004
1059
  installOutput: "",
1060
+ installFailed: false,
1061
+ keysProblems: [],
1005
1062
  reloadNeeded: false,
1006
1063
  confirm: ""
1007
1064
  };
@@ -1055,13 +1112,13 @@ window.__ModuleLoader__.load({
1055
1112
  passphrase: "",
1056
1113
  headers: view.config.headers,
1057
1114
  dirs: [...view.config.dirs ?? []],
1058
- dirsEnabled: view.config.dirsEnabled ?? true,
1115
+ dirsEnabled: view.config.dirsEnabled ?? false,
1059
1116
  rememberPassword: view.config.rememberPassword,
1060
1117
  autoEvery: view.config.autoEvery,
1061
1118
  models: view.config.models,
1062
- defaults: view.config.defaults,
1063
1119
  plugins: view.config.plugins,
1064
1120
  patch: view.config.patch,
1121
+ keys: view.config.keys ?? true,
1065
1122
  relayEnabled: view.config.relayEnabled ?? false,
1066
1123
  relayPort: view.config.relayPort ?? 3426,
1067
1124
  dirty: false
@@ -1215,6 +1272,7 @@ window.__ModuleLoader__.load({
1215
1272
  message: "",
1216
1273
  messageFailed: false,
1217
1274
  installOutput: "",
1275
+ installFailed: false,
1218
1276
  confirm: "",
1219
1277
  reloadNeeded: false
1220
1278
  });
@@ -1230,9 +1288,9 @@ window.__ModuleLoader__.load({
1230
1288
  headers: this.state.headers,
1231
1289
  autoEvery: this.state.autoEvery,
1232
1290
  models: this.state.models,
1233
- defaults: this.state.defaults,
1234
1291
  plugins: this.state.plugins,
1235
1292
  patch: this.state.patch,
1293
+ keys: this.state.keys,
1236
1294
  dirs: this.state.dirs,
1237
1295
  dirsEnabled: this.state.dirsEnabled,
1238
1296
  rememberPassword: this.state.rememberPassword,
@@ -1263,7 +1321,8 @@ window.__ModuleLoader__.load({
1263
1321
  sync() {
1264
1322
  this.run("sync", () => requestSync({ force: true }), (result) => ({
1265
1323
  preview: void 0,
1266
- reloadNeeded: result.reloadNeeded === true
1324
+ reloadNeeded: result.reloadNeeded === true,
1325
+ keysProblems: keyProblemsOf(result)
1267
1326
  })).then(() => this.load());
1268
1327
  }
1269
1328
  askRestore() {
@@ -1278,18 +1337,26 @@ window.__ModuleLoader__.load({
1278
1337
  undo() {
1279
1338
  this.run("undo", () => requestUndo(), (result) => ({
1280
1339
  reloadNeeded: result.reloadNeeded === true,
1281
- preview: void 0
1340
+ preview: void 0,
1341
+ keysProblems: keyProblemsOf(result)
1282
1342
  })).then(() => this.load());
1283
1343
  }
1284
1344
  install() {
1285
1345
  this.run("install", () => requestInstall(), (result) => ({
1286
1346
  installOutput: String(result.output ?? ""),
1287
- messageFailed: result.ok !== true
1347
+ installFailed: result.ok !== true,
1348
+ messageFailed: false
1288
1349
  })).then(() => this.load());
1289
1350
  }
1290
1351
  dismiss() {
1291
1352
  this.run("dismiss", () => requestDismiss(), () => ({})).then(() => this.load());
1292
1353
  }
1354
+ closeOutput() {
1355
+ this.set({
1356
+ installOutput: "",
1357
+ installFailed: false
1358
+ });
1359
+ }
1293
1360
  answer(yes) {
1294
1361
  const kind = this.state.confirm;
1295
1362
  this.set({ confirm: "" });
@@ -1297,7 +1364,8 @@ window.__ModuleLoader__.load({
1297
1364
  if (kind === "restore") {
1298
1365
  this.run("restore", () => requestRestore(), (result) => ({
1299
1366
  reloadNeeded: result.reloadNeeded === true,
1300
- preview: void 0
1367
+ preview: void 0,
1368
+ keysProblems: keyProblemsOf(result)
1301
1369
  })).then(() => this.load());
1302
1370
  return;
1303
1371
  }
@@ -1305,7 +1373,8 @@ window.__ModuleLoader__.load({
1305
1373
  this.run("upload", () => requestUpload(), (result) => ({
1306
1374
  preview: void 0,
1307
1375
  messageFailed: false,
1308
- reloadNeeded: result.reloadNeeded === true
1376
+ reloadNeeded: result.reloadNeeded === true,
1377
+ keysProblems: keyProblemsOf(result)
1309
1378
  })).then(() => this.load());
1310
1379
  return;
1311
1380
  }
@@ -1335,7 +1404,8 @@ window.__ModuleLoader__.load({
1335
1404
  askOff: () => this.askOff(),
1336
1405
  answer: (yes) => this.answer(yes),
1337
1406
  install: () => this.install(),
1338
- dismiss: () => this.dismiss()
1407
+ dismiss: () => this.dismiss(),
1408
+ closeOutput: () => this.closeOutput()
1339
1409
  };
1340
1410
  }
1341
1411
  };
@@ -1383,18 +1453,19 @@ window.__ModuleLoader__.load({
1383
1453
  partsLegend: "同步内容",
1384
1454
  partModels: "模型供应商与模型",
1385
1455
  partModelsHint: "有哪些供应商、各自提供哪些模型、API key 从哪里读。",
1386
- partDefaults: "默认模型",
1387
- partDefaultsHint: "新开会话时默认用哪个供应商、哪个模型。",
1388
1456
  partPlugins: "已安装插件",
1389
1457
  partPluginsHint: "装了哪些插件、按什么顺序加载。插件包本身不传,每台机器自己安装。",
1390
1458
  partPatch: "其余 profile 配置",
1391
1459
  partPatchHint: "MCP 服务器、权限预设、界面设置、insert 块等其余配置。",
1460
+ partKeys: "API 密钥",
1461
+ partKeysHint: "这台电脑上存着的 API 密钥,跟着点它名的那些供应商一起走;两边都改过时以本机为准,任何一次同步都不会删掉密钥。离开本机前同样用口令加密。",
1392
1462
  partDirs: "附加目录",
1393
1463
  partDirsHint: "profile 之外的目录,连里面的文件一起同步;路径跟着这份配置走,恢复时写回同一个目录。",
1394
1464
  partSynced: "已一致",
1395
1465
  partOnlyHere: "仅本机有改动",
1396
1466
  partOnlyThere: "仅服务器有改动",
1397
1467
  partBoth: "两边都有改动",
1468
+ partUnsaved: "待保存",
1398
1469
  lastCheck: "上次检查:{at}",
1399
1470
  neverChecked: "尚未检查过",
1400
1471
  lastSync: "上次同步:{at}",
@@ -1422,6 +1493,7 @@ window.__ModuleLoader__.load({
1422
1493
  pendingInstallAction: "安装",
1423
1494
  installOk: "安装完成。",
1424
1495
  installFailed: "安装未成功,请看下面的输出。",
1496
+ outputClose: "关闭",
1425
1497
  confirmRestore: "用服务器上的配置覆盖本机?本机现在的配置会先另存一份,之后可以撤销。",
1426
1498
  confirmUpload: "用本机配置覆盖服务器上的文件?服务器上现有的文件会先另存一份。",
1427
1499
  confirmOff: "关闭同步?服务器上的文件不会被删除。",
@@ -1440,6 +1512,8 @@ window.__ModuleLoader__.load({
1440
1512
  dirPlaceholder: "这个目录的绝对路径",
1441
1513
  dirRemove: "移除",
1442
1514
  dirProblem: "「{path}」这个目录没读成:{message}",
1515
+ keyProblem: "「{ref}」这个 API 密钥没读成:{message}",
1516
+ keyStoreProblem: "本机存的 API 密钥没读成:{message}",
1443
1517
  relayLegend: "本机转发",
1444
1518
  relayEnable: "启用本机转发",
1445
1519
  relayListening: "正在转发",
@@ -1479,18 +1553,19 @@ window.__ModuleLoader__.load({
1479
1553
  partsLegend: "What is synced",
1480
1554
  partModels: "Model providers and models",
1481
1555
  partModelsHint: "Which providers there are, what models each offers, and where their API keys come from.",
1482
- partDefaults: "Default model",
1483
- partDefaultsHint: "Which provider and model a new session starts on.",
1484
1556
  partPlugins: "Installed plugins",
1485
1557
  partPluginsHint: "Which plugins are installed and in what order they load. The packages themselves stay behind: each computer installs them.",
1486
1558
  partPatch: "Other profile configuration",
1487
1559
  partPatchHint: "MCP servers, permission presets, interface settings, insert blocks, and the rest.",
1560
+ partKeys: "API keys",
1561
+ partKeysHint: "The API keys stored on this computer, travelling with the providers that name them; when both sides changed, this computer’s copy wins, and no sync ever deletes a key. They are sealed with the passphrase before the file leaves this computer.",
1488
1562
  partDirs: "Extra directories",
1489
1563
  partDirsHint: "Directories outside the profile, carried with the files inside them; the paths travel with this configuration, and a restore writes back into the same directory.",
1490
1564
  partSynced: "In step",
1491
1565
  partOnlyHere: "Changed here only",
1492
1566
  partOnlyThere: "Changed on the server only",
1493
1567
  partBoth: "Changed on both",
1568
+ partUnsaved: "Not saved yet",
1494
1569
  lastCheck: "Last check: {at}",
1495
1570
  neverChecked: "Never checked yet",
1496
1571
  lastSync: "Last sync: {at}",
@@ -1518,6 +1593,7 @@ window.__ModuleLoader__.load({
1518
1593
  pendingInstallAction: "Install",
1519
1594
  installOk: "Installed.",
1520
1595
  installFailed: "The install did not finish; the output is below.",
1596
+ outputClose: "Close",
1521
1597
  confirmRestore: "Take the server’s setup over this computer’s? What is here now is kept aside first, so it can be undone.",
1522
1598
  confirmUpload: "Write this computer’s setup over the file on the server? The file there is kept aside first.",
1523
1599
  confirmOff: "Stop syncing? The file on the server is not deleted.",
@@ -1536,6 +1612,8 @@ window.__ModuleLoader__.load({
1536
1612
  dirPlaceholder: "Absolute path of the directory",
1537
1613
  dirRemove: "Remove",
1538
1614
  dirProblem: "Could not read the directory {path}: {message}",
1615
+ keyProblem: "Could not read the API key {ref}: {message}",
1616
+ keyStoreProblem: "Could not read the stored API keys: {message}",
1539
1617
  relayLegend: "Forwarding on this computer",
1540
1618
  relayEnable: "Forward for other programs on this computer",
1541
1619
  relayListening: "Forwarding",
package/lib/dirs.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * The extra directories: reading them into the `dirs` part, and writing that
3
3
  * part back out.
4
4
  *
5
- * The four profile parts are read from two files whose shape this plugin already
5
+ * The profile parts are read from two files whose shape this plugin already
6
6
  * knows. This one is different in three ways, and each of them shapes the code
7
7
  * below:
8
8
  *
package/lib/dirs.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * The extra directories: reading them into the `dirs` part, and writing that
3
3
  * part back out.
4
4
  *
5
- * The four profile parts are read from two files whose shape this plugin already
5
+ * The profile parts are read from two files whose shape this plugin already
6
6
  * knows. This one is different in three ways, and each of them shapes the code
7
7
  * below:
8
8
  *
package/lib/engine.d.ts CHANGED
@@ -13,27 +13,44 @@
13
13
  * with 409, a write that went out without the server checking its version, a body
14
14
  * a relay cut in half.
15
15
  */
16
- import { type Bundle, type CheckResult, type ConfigView, type Part, type PartHashes, type PreviewResult, type StatusResult, type SyncConfig, type SyncResult } from './wire.js';
16
+ import { type Bundle, type CheckResult, type ConfigView, type Part, type PartHashes, type KeyProblem, type PreviewResult, type StatusResult, type SyncConfig, type SyncResult } from './wire.js';
17
17
  import { type DirsRead } from './dirs.js';
18
18
  import { missingPlugins, type Collected, type ManifestFile, type PatchFile } from './store.js';
19
19
  import { loadConfig, sameAccount, saveConfig, usable } from './state.js';
20
20
  import type { ResolvedPaths } from './paths.js';
21
+ import { type KeysPort } from './keys.js';
21
22
  /** One read of this computer's setup, with the files it came from. */
22
23
  interface Snapshot {
23
24
  patch: PatchFile;
24
25
  manifest: ManifestFile;
25
- /** The four parts that come out of the profile's own two files. */
26
+ /** The parts that come out of the profile's own two files, plus the keys. */
26
27
  parts: Collected;
27
28
  /** The extra directories, walked from disk, with whatever could not be read. */
28
29
  dirs: DirsRead;
30
+ /**
31
+ * Credential references this read could not resolve, with the reason.
32
+ *
33
+ * Kept on the snapshot rather than inside `parts` for the same reason the
34
+ * directories keep theirs beside the part: a store that cannot be read must not
35
+ * travel as "this computer has no keys". See {@link readKeys}.
36
+ */
37
+ keysProblems: KeyProblem[];
29
38
  }
30
39
  /** Everything a sync needs, resolved once. */
31
40
  export interface Engine {
32
41
  paths: ResolvedPaths;
33
42
  config: SyncConfig;
43
+ /**
44
+ * The host's credential store, absent on a host without one.
45
+ *
46
+ * Carried on the engine for the same reason the paths are: it is the one thing
47
+ * a call needs that no configuration can say. Absent is a working state — the
48
+ * `keys` part then has nothing to read and nothing to write, and says so.
49
+ */
50
+ credentials?: KeysPort | undefined;
34
51
  }
35
- /** The engine for a profile directory and a configuration. */
36
- export declare function engineFor(paths: ResolvedPaths, config: SyncConfig): Engine;
52
+ /** The engine for a profile directory, a configuration, and the host's credential store. */
53
+ export declare function engineFor(paths: ResolvedPaths, config: SyncConfig, credentials?: KeysPort | undefined): Engine;
37
54
  /**
38
55
  * Run one merge.
39
56
  *
@@ -89,7 +106,7 @@ export declare function check(engine: Engine): Promise<CheckResult>;
89
106
  /** What one sync would do, decided the same way but writing nothing anywhere. */
90
107
  export declare function preview(engine: Engine): Promise<PreviewResult>;
91
108
  /** Read this computer's setup and the files it came from, in one pass. */
92
- export declare function take(paths: ResolvedPaths, config: SyncConfig): Promise<Snapshot>;
109
+ export declare function take(paths: ResolvedPaths, config: SyncConfig, credentials?: KeysPort | undefined): Promise<Snapshot>;
93
110
  /** Per-part hashes of a setup; a part that is not there has none. */
94
111
  export declare function hashesOf(parts: Partial<Record<Part, unknown>> | Bundle): PartHashes;
95
112
  /** Whether two sets of part hashes describe the same setup. */