vault prompt: appears at once and stays put between questions

The password box rose into place like the [y/N] prompt, a 380 ms slide
and fade, and was taken down and put up again for every question: twice
for :vault, three times for :passwd, and right after the command line
closed for :unlock. Whatever was typed in that time went into a field
still moving and half transparent.

It now appears without an entrance, and an answer puts it away a turn
later, so a question that follows straight on takes over the same box.
Password managers are asked to leave the field alone; it is not the
login.
This commit is contained in:
nvrl committed 2026-10-20 11:00:08 +02:00
1 parent e4e3e2b5aa
commit b19fb21fc4
2 files changed
+23 -5

No files matched your search

+10 -1
View File
@@ -3,6 +3,12 @@
* The password `workspace.askSecret` asks for, in the same place as the
* `[y/N]` prompt. A masked field of its own rather than the command line, so
* a password never shows on screen or lands in the command history.
*
* It appears at once rather than rising into place like the `[y/N]` prompt:
* it is where the next keys go, and a field still sliding and fading in
* under them reads as a stutter. Password managers are asked to leave the
* field alone: it is not the login, and their menus redrawn on every key
* are a flicker of their own.
*/
import { workspace } from './store.svelte'
@@ -38,6 +44,10 @@
type="password"
autocomplete="off"
spellcheck="false"
data-1p-ignore
data-lpignore="true"
data-bwignore
data-form-type="other"
bind:this={input}
bind:value
{onkeydown}
@@ -67,7 +77,6 @@
border-bottom: 0;
border-radius: var(--radius-l) var(--radius-l) 0 0;
font-size: 13px;
animation: rise var(--t-slow) var(--ease-snap) both;
}
.text { flex: none; color: var(--light); }
input {
+13 -4
View File
@@ -125,7 +125,7 @@ class Workspace {
*/
prompt = $state<{ text: string; resolve: (yes: boolean) => void } | null>(null)
/** A password asked for in the same place, typed into a masked field. */
secret = $state<{ text: string; resolve: (value: string | null) => void } | null>(null)
secret = $state.raw<{ text: string; resolve: (value: string | null) => void } | null>(null)
/** The vault, as last heard from the server; null until asked. */
vault = $state<VaultStatus | null>(null)
media = $state<MediaFile[]>([])
@@ -217,13 +217,22 @@ class Workspace {
askSecret(text: string): Promise<string | null> {
this.secret?.resolve(null)
return new Promise((resolve) => {
this.secret = {
let answered = false
const asked = {
text,
resolve: (value) => {
this.secret = null
resolve: (value: string | null) => {
if (answered) return
answered = true
// Put away a turn later, so a question that follows straight on —
// the second of two passwords — takes over the same box instead
// of a new one appearing under the fingers already typing it.
setTimeout(() => {
if (this.secret === asked) this.secret = null
})
resolve(value)
},
}
this.secret = asked
})
}