UI/UX interaction conventions for sdk-parity features
This issue is the single source of truth for how the ~22 sdk-parity feature issues (#56–#78) surface their UI in b2-vscode. Every new feature MUST conform to the rulings below. PRs that diverge should be sent back to match the relevant family pattern.
Native-first principle
The extension today uses only native VS Code primitives — zero webviews, zero custom editors, zero virtual-document providers. Files are viewed by downloading to a temp file and opening via vscode.open. Menu gating is done with setContext keys + when clauses. The default bar is: stay native. A webview or custom editor is a new, heavier dependency and is presumed non-compliant unless it passes the three-part gate below.
Current vocabulary (precedent set)
- TreeView:
createTreeView for b2Buckets (src/extension.ts), single-select today.
- Transient input:
showInputBox, showQuickPick, showInformationMessage/showWarningMessage/showErrorMessage (src/commands/index.ts).
- Progress + cancellation:
withProgress(Notification) / withCancellableTransferProgress (src/services/transferProgress.ts).
- Destructive confirm:
{ modal: true } — irreversible actions only.
- Clipboard / open:
env.clipboard.writeText, vscode.open (src/commands/index.ts).
- Gating:
setContext (src/services/authService.ts:101, key b2.authenticated) + when clauses; existing config-mutation precedent src/commands/publicBucketVisibility.ts.
- Status bar:
src/ui/statusBar.ts. Menu groups: navigation, 1_copy, 2_bucket, 3_destructive, inline; palette-hidden item-commands via commandPalette "when": false.
Webview / custom-editor gate
A webview/custom editor is allowed ONLY if it answers YES to all three: (1) content is genuinely visual or interactive (charts, image preview, drag-reorder canvas) — not a form, property list, or JSON; (2) it cannot be expressed natively without material loss of function; (3) it does not duplicate a Family A (config-edit) or Family B (detail-view) need. Forms, config editing, key creation, property/detail views, and list browsing are never valid webview justifications. No current issue passes the gate — the warranted-webview set is empty.
Family rulings
Family A — bucket config-edit (#65, #66, #67, #68, #70, #78). Edit-as-JSON-document, native, no webview. Ship the identical trio on the shared b2-config: scheme:
B2: Show <Config>… — read-only virtual JSON doc via one shared TextDocumentContentProvider, URI b2-config:/<bucketName>/<kind>.json, languageId: jsonc. Diffable/copyable/scriptable read path.
B2: Edit <Config>… — editable untitled jsonc doc seeded with the current config + a comment banner naming the bucket, config kind, and apply command; sets a b2.editingConfig:<kind> context key.
B2: Apply <Config> — editor-title action gated by when on that key: parse → validate against the schema → abort with showErrorMessage and no partial write on failure → apply inside withProgress(Notification) → refresh → success message. Destructive deltas (clear-all / replace ruleset) require { modal: true }.
Mandatory for array/nested/free-map configs. The ≤~3-enumerable-field QuickPick downgrade applies ONLY to #69 default-SSE (which follows the changeBucketVisibility QuickPick+modal variant, not the JSON trio).
Family B — rich read-only detail (#61, #77). ONE shared b2-detail: TextDocumentContentProvider, rendered as Markdown property tables, opened ViewColumn.Beside, read-only. Per-field copy via env.clipboard.writeText. No webview, no showInformationMessage wall-of-text. #61's optional content-type/fileInfo edit is a separate Family A (edit-as-JSON) flow.
list-browse (#59, #63, #64, #71). Extend the TreeView; never a QuickPick-as-list or webview. Child-nodes when data is intrinsically part of a node (#59 versions); a NEW sibling view inside the existing b2Explorer container for top-level collections (#64 keys, #71 unfinished uploads). #63 search filters the existing b2Buckets tree (reusing LoadMoreTreeItem/ListingLimitTreeItem), never a QuickPick.
file-action (#58, #60). Native context-menu commands + withProgress + refresh(); reuse renameFile copy-then-delete + B2PartialFailureError. { modal: true } only for irreversible deletes (not reversible hide). #60 unhide is paired with #59 (needs a hide-marker node) or setContext-gated.
file-transfer / file-I/O (#56, #57, #72). Native showOpenDialog/showSaveDialog (no silent temp writes) + withCancellableTransferProgress honoring the AbortSignal; TreeDragAndDropController on b2Buckets for OS-file drops; sync dry-run plan in a modal summary + OutputChannel/untitled doc.
bulk-multiselect (#73). canSelectMany:true set once on b2Buckets; commands accept (item, items[]); single modal count-summarized confirmation; one cancelable withProgress; per-item failures summarized without aborting. #57/#58/#71 compose against this rather than re-adding it.
clipboard-export / secret-display (#62, #75; secret pattern also #64, #69). env.clipboard.writeText + showInformationMessage in 1_copy for non-secrets; modal+Copy or untitled/discardable editor for secrets — never persisted, never logged, never in settings/globalState.
capability-gating (#76). setContext keys in src/constants.ts, set/cleared in AuthService.setAuthState, consumed by when clauses. Never imperative greying or showErrorMessage('not allowed').
Per-issue table
| Issue |
Family |
Primary primitive |
Native? |
Note |
| #56 |
file-transfer |
showOpenDialog + withProgress + TreeDragAndDropController |
yes |
Upload direction; modal overwrite confirm. |
| #57 |
file-transfer |
showSaveDialog/showOpenDialog + withCancellableTransferProgress |
yes |
First real save/open dialog; checksum-abort. |
| #58 |
file-action |
multi-step showQuickPick dest + withProgress copy/delete |
yes |
Reuse renameFile + B2PartialFailureError. |
| #59 |
list-browse |
TreeView child-nodes under file |
yes |
Versions belong to a file; not a QuickPick. |
| #60 |
file-action |
view/item/context commands + withProgress + refresh |
yes |
Hide = reversible (no modal); unhide paired with #59. |
| #61 |
detail-view |
b2-detail: read-only virtual doc (Markdown) |
yes |
Same provider/format as #77; edit path = Family A. |
| #62 |
clipboard-export |
showQuickPick TTL + clipboard.writeText |
yes |
Token is a one-time secret (§6). |
| #63 |
search/list-browse |
filter the b2Buckets TreeView |
yes |
Drop the QuickPick branch; reuse LoadMore. |
| #64 |
multistep-create + list-browse + secret |
new b2Keys view + multi-step QuickPick + modal+Copy |
yes |
§5 creation anchor; sibling view in b2Explorer. |
| #65 |
config-edit (A) |
edit-as-JSON trio (b2-config:) |
yes |
Array config → JSON mandatory. |
| #66 |
config-edit (A) |
edit-as-JSON trio (b2-config:) |
yes |
Validate ops vs CorsOperation. |
| #67 |
config-edit (A) |
edit-as-JSON trio (b2-config:) |
yes |
Nested rules → JSON mandatory; modal on clear. |
| #68 |
config-edit (A) |
edit-as-JSON trio (b2-config:) |
yes |
Drop ad-hoc per-field chain; HMAC secret in JSON not logged. |
| #69 |
config-edit + file-transfer |
showQuickPick (SSE mode) + password InputBox + withProgress |
yes |
Bounded enumerable → changeBucketVisibility variant, NOT JSON trio. |
| #70 |
config-edit + file-action |
JSON trio (default retention) + multi-step QuickPick (per-file) |
yes |
Default retention = Family A; governance bypass = setContext-gated modal. |
| #71 |
list-browse |
new b2UnfinishedUploads view (canSelectMany) |
yes |
Sibling view; modal cancel/cancel-all. |
| #72 |
file-transfer |
multi-step QuickPick + showOpenDialog + withCancellableTransferProgress |
yes |
Dry-run modal + OutputChannel, no webview. |
| #73 |
bulk-multiselect |
canSelectMany + (item, items[]) commands |
yes |
Shared prerequisite for bulk download/move. |
| #75 |
secret/clipboard-export |
modal+Copy / untitled ini doc + clipboard.writeText |
yes |
Pairs with #77; §6 secret handling. |
| #76 |
capability-gating |
setContext keys + when clauses |
yes |
Gating mechanism every conditional feature reuses. |
| #77 |
detail-view |
b2-detail: read-only virtual doc (Markdown) |
yes |
Same provider/format as #61; pure read-only. |
| #78 |
config-edit (A) |
edit-as-JSON trio (b2-config:) |
yes |
Free-form map; split single command into Show/Edit/Apply. |
All 22 issues resolve to native primitives. No webview or custom editor is warranted.
Update — fitness-neutral re-pass
A second audit re-evaluated every feature with the native-first bias removed: native vs. custom-editor vs. webview scored on fitness, each recommendation run through a symmetric adversarial challenge (webview picks had to survive "native suffices"; native picks had to survive "this is forced/degraded"). Honest result: of 22 features, 17 confirm native as the genuine best fit, 0 in-scope issues warrant a webview/custom editor, with 5 "right call, wrong native primitive" corrections and 1 cross-cutting webview capability (filed as #101).
Corrections to the rulings above — all stay native
| # |
Was |
Now |
Why |
| #67 |
edit raw JSON |
multi-step QuickPick + live bucket/key pickers |
JSON enums can't bind live account resources (destinationBucketId, sourceApplicationKeyId) |
| #72 |
modal + OutputChannel |
checkbox TreeView plan (select before apply) |
a destructive delete-by-prefix plan must be actionable, not a read-only dump |
| #77 |
read-only Markdown |
QuickPick with copy-per-item |
Markdown fails the issue's own "copy each endpoint" criterion |
| #70 |
JSON as default |
multi-step QuickPick; JSON = opt-in escape hatch |
JSON overkill for a 2-field retention policy |
| #61 |
read-only Markdown |
hybrid read + light edit |
the read-only doc had no edit path for content-type/fileInfo |
| #65 |
Show/Edit/Apply trio |
text-backed editor (save persists, reload re-reads) |
trio slightly overbuilt; JSON+schema family still correct |
Each corrected issue carries a detailed comment.
The one genuine webview case → #101
A unified multi-transfer manager dashboard (uploads + downloads + sync + batch, with per-item pause/resume/throughput). withProgress is hard-limited to one task per call — a real native dead-end. It is cross-cutting (no single issue covers it), so it is filed separately as #101, and must degrade on web/remote hosts (#5).
Caveat: the re-pass agents read the issue requirements via gh but ran in a different working directory and could not grep the extension source, so the 5 re-homings should be sanity-checked against the real code (only the de-weighted "consistency" criterion is affected).
When native is the wrong choice (and when it isn't)
When native is the wrong choice (and when it isn't)
Native VS Code primitives (TreeView, QuickPick, InputBox, withProgress, virtual documents, JSON+schema, context-menu commands, setContext/when) are the right default. They are free, themed, accessible, keyboard-native, and fully portable across web/remote/virtual hosts (issue #5). A webview or custom editor should be reached for only when a specific, load-bearing signal fires — and never to "look richer."
The five signals that justify a non-native surface
A richer surface (webview / text-backed custom editor) is warranted only when at least one of these is genuinely present at the issue's defined scope:
- Information density / simultaneity — the user must reason over many panels or fields at once, and a one-at-a-time QuickPick or a flat document materially degrades the task.
- Many interdependent / conditional fields (the QuickPick cliff) — roughly >5 interdependent fields, or conditional layout where one choice re-shapes others. (Independent fields, even ~5 of them with simple Back-navigation, stay inside the QuickPick envelope.)
- Inherent visual / graphical / spatial content — charts, topology, drag-to-reorder canvases, timelines. A scalar (a count, a size, a part number) is not graphical.
- Live multi-item dashboard state — many concurrent items updating in real time with per-item controls (pause/resume/retry/throughput). A static inventory (a list you fetch and act on) is not this.
- A real, persistent, savable/diffable document type — and even then this points to a text-backed custom editor, not a webview.
If none fire, native is not merely adequate — it is the highest-fitness answer, and a webview is pure cost (CSP hardening, bundle, message-passing, lifecycle, re-implemented theming/a11y) plus a host-portability regression against issue #5.
The trap to avoid first: the weak native primitive
The most common real defect is not "should have been a webview" — it is right family, wrong native member. Before overturning native to a richer surface, check that you picked the fit native primitive:
Two security-specific rules
The host-portability tiebreaker (#5)
When a webview and native tie on raw capability, native wins because issue #5 targets web/remote/virtual hosts where a webview is constrained and heavier. Storage and credential-minting flows in particular must keep working in Codespaces/vscode.dev, so the tie never breaks toward the webview.
The honest exception
The one place a webview is genuinely correct in this codebase is not any single feature — it is the cross-cutting transfer manager (concurrent up/downloads + sync + batch with per-item pause/resume/cancel and live throughput). withProgress is hard-limited to one task per call and cannot host a live multi-item dashboard. That is signal #4 firing for real, across four otherwise-native features — and it deserves its own scoped issue rather than bloating any of them.
UI/UX interaction conventions for sdk-parity features
This issue is the single source of truth for how the ~22 sdk-parity feature issues (#56–#78) surface their UI in
b2-vscode. Every new feature MUST conform to the rulings below. PRs that diverge should be sent back to match the relevant family pattern.Native-first principle
The extension today uses only native VS Code primitives — zero webviews, zero custom editors, zero virtual-document providers. Files are viewed by downloading to a temp file and opening via
vscode.open. Menu gating is done withsetContextkeys +whenclauses. The default bar is: stay native. A webview or custom editor is a new, heavier dependency and is presumed non-compliant unless it passes the three-part gate below.Current vocabulary (precedent set)
createTreeViewforb2Buckets(src/extension.ts), single-select today.showInputBox,showQuickPick,showInformationMessage/showWarningMessage/showErrorMessage(src/commands/index.ts).withProgress(Notification)/withCancellableTransferProgress(src/services/transferProgress.ts).{ modal: true }— irreversible actions only.env.clipboard.writeText,vscode.open(src/commands/index.ts).setContext(src/services/authService.ts:101, keyb2.authenticated) +whenclauses; existing config-mutation precedentsrc/commands/publicBucketVisibility.ts.src/ui/statusBar.ts. Menu groups:navigation,1_copy,2_bucket,3_destructive,inline; palette-hidden item-commands viacommandPalette"when": false.Webview / custom-editor gate
A webview/custom editor is allowed ONLY if it answers YES to all three: (1) content is genuinely visual or interactive (charts, image preview, drag-reorder canvas) — not a form, property list, or JSON; (2) it cannot be expressed natively without material loss of function; (3) it does not duplicate a Family A (config-edit) or Family B (detail-view) need. Forms, config editing, key creation, property/detail views, and list browsing are never valid webview justifications. No current issue passes the gate — the warranted-webview set is empty.
Family rulings
Family A — bucket config-edit (#65, #66, #67, #68, #70, #78). Edit-as-JSON-document, native, no webview. Ship the identical trio on the shared
b2-config:scheme:B2: Show <Config>…— read-only virtual JSON doc via one sharedTextDocumentContentProvider, URIb2-config:/<bucketName>/<kind>.json,languageId: jsonc. Diffable/copyable/scriptable read path.B2: Edit <Config>…— editable untitledjsoncdoc seeded with the current config + a comment banner naming the bucket, config kind, and apply command; sets ab2.editingConfig:<kind>context key.B2: Apply <Config>— editor-title action gated bywhenon that key: parse → validate against the schema → abort withshowErrorMessageand no partial write on failure → apply insidewithProgress(Notification)→ refresh → success message. Destructive deltas (clear-all / replace ruleset) require{ modal: true }.Mandatory for array/nested/free-map configs. The ≤~3-enumerable-field QuickPick downgrade applies ONLY to #69 default-SSE (which follows the
changeBucketVisibilityQuickPick+modal variant, not the JSON trio).Family B — rich read-only detail (#61, #77). ONE shared
b2-detail:TextDocumentContentProvider, rendered as Markdown property tables, openedViewColumn.Beside, read-only. Per-field copy viaenv.clipboard.writeText. No webview, noshowInformationMessagewall-of-text. #61's optional content-type/fileInfoedit is a separate Family A (edit-as-JSON) flow.list-browse (#59, #63, #64, #71). Extend the TreeView; never a QuickPick-as-list or webview. Child-nodes when data is intrinsically part of a node (#59 versions); a NEW sibling view inside the existing
b2Explorercontainer for top-level collections (#64 keys, #71 unfinished uploads). #63 search filters the existingb2Bucketstree (reusingLoadMoreTreeItem/ListingLimitTreeItem), never a QuickPick.file-action (#58, #60). Native context-menu commands +
withProgress+refresh(); reuserenameFilecopy-then-delete +B2PartialFailureError.{ modal: true }only for irreversible deletes (not reversible hide). #60 unhide is paired with #59 (needs a hide-marker node) or setContext-gated.file-transfer / file-I/O (#56, #57, #72). Native
showOpenDialog/showSaveDialog(no silent temp writes) +withCancellableTransferProgresshonoring the AbortSignal;TreeDragAndDropControlleronb2Bucketsfor OS-file drops; sync dry-run plan in a modal summary + OutputChannel/untitled doc.bulk-multiselect (#73).
canSelectMany:trueset once onb2Buckets; commands accept(item, items[]); single modal count-summarized confirmation; one cancelablewithProgress; per-item failures summarized without aborting. #57/#58/#71 compose against this rather than re-adding it.clipboard-export / secret-display (#62, #75; secret pattern also #64, #69).
env.clipboard.writeText+showInformationMessagein1_copyfor non-secrets; modal+Copy or untitled/discardable editor for secrets — never persisted, never logged, never in settings/globalState.capability-gating (#76).
setContextkeys insrc/constants.ts, set/cleared inAuthService.setAuthState, consumed bywhenclauses. Never imperative greying orshowErrorMessage('not allowed').Per-issue table
All 22 issues resolve to native primitives. No webview or custom editor is warranted.
Update — fitness-neutral re-pass
A second audit re-evaluated every feature with the native-first bias removed: native vs. custom-editor vs. webview scored on fitness, each recommendation run through a symmetric adversarial challenge (webview picks had to survive "native suffices"; native picks had to survive "this is forced/degraded"). Honest result: of 22 features, 17 confirm native as the genuine best fit, 0 in-scope issues warrant a webview/custom editor, with 5 "right call, wrong native primitive" corrections and 1 cross-cutting webview capability (filed as #101).
Corrections to the rulings above — all stay native
destinationBucketId,sourceApplicationKeyId)fileInfoEach corrected issue carries a detailed comment.
The one genuine webview case → #101
A unified multi-transfer manager dashboard (uploads + downloads + sync + batch, with per-item pause/resume/throughput).
withProgressis hard-limited to one task per call — a real native dead-end. It is cross-cutting (no single issue covers it), so it is filed separately as #101, and must degrade on web/remote hosts (#5).When native is the wrong choice (and when it isn't)
When native is the wrong choice (and when it isn't)
Native VS Code primitives (TreeView, QuickPick, InputBox, withProgress, virtual documents, JSON+schema, context-menu commands,
setContext/when) are the right default. They are free, themed, accessible, keyboard-native, and fully portable across web/remote/virtual hosts (issue #5). A webview or custom editor should be reached for only when a specific, load-bearing signal fires — and never to "look richer."The five signals that justify a non-native surface
A richer surface (webview / text-backed custom editor) is warranted only when at least one of these is genuinely present at the issue's defined scope:
If none fire, native is not merely adequate — it is the highest-fitness answer, and a webview is pure cost (CSP hardening, bundle, message-passing, lifecycle, re-implemented theming/a11y) plus a host-portability regression against issue #5.
The trap to avoid first: the weak native primitive
The most common real defect is not "should have been a webview" — it is right family, wrong native member. Before overturning native to a richer surface, check that you picked the fit native primitive:
QuickPickItemButtons/ accept-to-copy — not a read-only virtual document (which can display but not copy). (cf. Add "Account Info" view exposing authorization details (account ID, endpoints, restrictions) #77)TreeItemCheckboxState+canSelectMany) so the blast radius is both visible and deselectable — not a modal + read-only OutputChannel the user can only accept whole. (cf. Local to B2 sync (mirror a workspace folder to or from a bucket) #72)Two security-specific rules
password:trueInputBox + SecretStorage, or an untitled doc that never touches disk. A webview here is a security regression, not a feature. (cf. Server-side encryption support (SSE-B2 and SSE-C, per-object and bucket default) #69, Application key management view (create, list, delete with capabilities and scope) #64, Surface S3-compatible access (endpoint + credentials for external S3 tools) #75)The host-portability tiebreaker (#5)
When a webview and native tie on raw capability, native wins because issue #5 targets web/remote/virtual hosts where a webview is constrained and heavier. Storage and credential-minting flows in particular must keep working in Codespaces/vscode.dev, so the tie never breaks toward the webview.
The honest exception
The one place a webview is genuinely correct in this codebase is not any single feature — it is the cross-cutting transfer manager (concurrent up/downloads + sync + batch with per-item pause/resume/cancel and live throughput).
withProgressis hard-limited to one task per call and cannot host a live multi-item dashboard. That is signal #4 firing for real, across four otherwise-native features — and it deserves its own scoped issue rather than bloating any of them.