Skip to content

Add MiniFileChooser component (CS-11680)#5298

Open
FadhlanR wants to merge 8 commits into
mainfrom
cs-11680-mini-file-chooser
Open

Add MiniFileChooser component (CS-11680)#5298
FadhlanR wants to merge 8 commits into
mainfrom
cs-11680-mini-file-chooser

Conversation

@FadhlanR

@FadhlanR FadhlanR commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Background and Goal

Second primitive in the Markdown Editing UI sequence, sibling to MiniCardChooser (CS-11672). MiniFileChooser is a compact, standalone, independently-mountable file picker sized to match the mini card chooser, so the two can sit side-by-side in the combined modal a later ticket builds.

It renders a Workspace dropdown over the indexed file tree, plus an Upload… button and drag-and-drop upload. It reuses the machinery already proven in ChooseFileModalRealmDropdown, IndexedFileTree, and the file-upload service — rather than reimplementing any of it. The primitive only fires onSelect(url) with the picked or uploaded file's absolute URL; the hosting container owns confirmation/dismissal (no footer / Add / Cancel), matching MiniCardChooser.

File-embed serialization decision

The ticket asked us to decide and document how downstream tickets serialize file embeds, framing it as :card[URL] vs a plain markdown link [name](URL), and said do not add new BFM syntax.

Decision: serialize file embeds as :file[URL] (block ::file[URL]).

This is not new syntax — BFM already has a dedicated, purpose-built file reference that resolves to a FileDef and renders as an embed:

  • packages/host/app/lib/codemirror-context.tsBLOCK_FILE_RE / INLINE_FILE_RE, refType: 'card' | 'file'
  • packages/host/app/components/operator-mode/preview-panel/rendered-markdown.gtsextractFileReferenceUrls, RenderSlot.refType === 'file'

It is strictly better than the two framed options: :card[URL] would route a file through card rendering even though FileDef has dedicated file rendering, and a plain [name](URL) link renders as an anchor, not an embed. MiniFileChooser itself returns only a URL; downstream tickets apply the :file[URL] serialization.

Where to start

  • packages/host/app/components/file-chooser/mini/index.gts — the component. Note selectFile (resolves the tree path to an absolute URL via RealmPaths.fileURL) and beginUpload (fires onSelect with FileDef.sourceUrl once upload completes).
  • packages/host/app/components/file-chooser/mini/usage.gts + the host-freestyle.gts registration — Freestyle entry for manual review.

Key decisions and non-obvious mechanics

  • Workspace switch recreates the tree. A nonce+realm render key forces IndexedFileTree to be torn down and rebuilt when the realm changes, mirroring ChooseFileModal.
  • @selected highlight. The user's in-tree pick wins; otherwise we derive a LocalPath from @selected only when it lives inside the open workspace (RealmPaths.inRealm / .local).
  • Drag-and-drop reuses the modal's drop-zone treatment (dim overlay + Drop file to upload to <realm> label) via the data-drop-zone-active / data-drop-zone-label attributes.

Screenshot

Screenshot 2026-06-23 at 13 44 34

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ± 0      1 suites  ±0   2h 1m 43s ⏱️ + 1m 45s
3 204 tests +18  3 189 ✅ +18  15 💤 ±0  0 ❌ ±0 
3 223 runs  +18  3 208 ✅ +18  15 💤 ±0  0 ❌ ±0 

Results for commit fb177eb. ± Comparison against earlier commit b8226b9.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   12m 7s ⏱️ + 1m 45s
1 733 tests ±0  1 732 ✅  - 1  0 💤 ±0  1 ❌ +1 
1 826 runs  ±0  1 825 ✅  - 1  0 💤 ±0  1 ❌ +1 

Results for commit fb177eb. ± Comparison against earlier commit b8226b9.

For more details on these errors, see this check.

FadhlanR and others added 4 commits June 23, 2026 12:37
Compact, standalone file picker sized to match MiniCardChooser: a
workspace dropdown over the indexed file tree, plus an Upload… button
and drag-and-drop upload. Reuses RealmDropdown, IndexedFileTree, and the
file-upload service. Fires onSelect with the picked or uploaded file URL;
the hosting container owns confirmation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Guard against no known realms (the Freestyle smoke check renders the
  usage with no realms loaded) so the component renders without
  dereferencing an undefined selectedRealm.
- Glimmer renders a true boolean attribute with an empty value, so match
  the drop-zone overlay with a presence selector (as choose-file-modal
  does) instead of [data-drop-zone-active='true'].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pull the realm selection, file-tree recreation key, upload lifecycle, and
drag-and-drop state machine out of the mini chooser and the operator-mode
choose-file modal into a single renderless FileChooser provider
(file-chooser/panel.gts) that yields curried RealmDropdown/FileTree
components plus the shared state and actions.

Rewrite mini/index.gts to consume the panel, and replace
operator-mode/choose-file-modal.gts with file-chooser/modal.gts, preserving
the _CARDSTACK_FILE_CHOOSER global, chooseFile() API, and all
data-test-choose-file-modal* hooks so existing callers and tests are
unaffected. The migrated modal now clears the staged file on workspace
switch, so the Add button can no longer resolve a path against the wrong
realm.

Add an integration test covering the panel's initial-realm selection,
realm-switch notification + recreation-key bump, and file-only drop-zone
gating.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion

The freestyle smoke check rendered MiniFileChooser — the first freestyle
usage to mount a RealmDropdown — during app boot. RealmDropdown.selectedRealm
fell back to realm.defaultWritableRealm, which reads matrixService.userName
and throws "cannot use matrix client before matrix SDK has loaded" while the
realm list is still loading. Short-circuit selectedRealm to undefined when the
realm list is empty; the fallback resolved to undefined against an empty list
anyway, so this is behavior-preserving.

Also fix the file-chooser panel test: a bound boolean true renders as an
empty-valued attribute, so assert data-drop-zone-active="" (the CSS uses it as
a presence flag) rather than "true".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FadhlanR FadhlanR force-pushed the cs-11680-mini-file-chooser branch from b83539a to cfa552c Compare June 23, 2026 05:37
Clicking a file in the tree moved focus into the nav, and the
:focus-within rule drew a teal outline around the whole tree box on every
mouse pick. Gate the ring on :has(:focus-visible) so it appears for keyboard
navigation only, not on click.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FadhlanR FadhlanR marked this pull request as ready for review June 23, 2026 06:44
@FadhlanR FadhlanR requested a review from a team June 23, 2026 06:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b20815bf6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +126 to +129
@onFileConfirmed={{fn
this.handleFileSelected
chooser.selectedRealm
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid firing onSelect twice for Enter

When the file tree has focus and the user presses Enter on a file, IndexedFileTree.handleKeydown calls selectFile() and then onFileConfirmed. Because the mini chooser wires both callbacks to handleFileSelected here, one keyboard activation invokes @onSelect twice, which can duplicate whatever the host does with that URL, such as inserting two file embeds.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 20f6f99. MiniFileChooser now routes @onFileSelected to a handleFileSelectOnly action that only updates the highlighted row, and routes @onFileConfirmed to handleFileSelected which is the sole caller of onSelect. Click highlights; Enter confirms and fires onSelect once. The integration test was updated to click-then-Enter.

Comment on lines +71 to +72
@tracked private selectedRealm: FileChooserRealm | undefined =
this.initialRealm;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update selected realm when realms arrive

If this panel mounts before realm.allRealmsInfo has been populated, initialRealm is undefined and this tracked field stays undefined permanently; later realm-service updates do not rerun the initializer. In that boot/loading scenario the yielded chooser.selectedRealm remains empty, so consumers never render the file tree or have a realm to upload into until the component is recreated.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 20f6f99. selectedRealm is now a getter returning _explicitlySelectedRealm ?? initialRealm. initialRealm reads knownRealms (which depends on realm.allRealmsInfo), so a panel that mounted before the realm list populated picks up a realm reactively as soon as it loads — selectedRealmURL, the yielded tree, upload, drop, and the drop-zone label all follow the same getter.

@jurgenwerk jurgenwerk requested a review from Copilot June 23, 2026 07:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Comment on lines +126 to +130
@onFileConfirmed={{fn
this.handleFileSelected
chooser.selectedRealm
}}
@autoFocus={{true}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 20f6f99. MiniFileChooser now wires @onFileSelected to handleFileSelectOnly (highlight-only, no onSelect) and @onFileConfirmed to handleFileSelected (the only path that fires onSelect). Enter no longer double-fires.

Comment on lines +95 to +101
private get selectedRealmURL(): string | undefined {
return this.selectedRealm?.url.href;
}

private get fileTreeKey(): string {
return `${this.fileTreeRenderNonce}:${this.selectedRealm?.url.href ?? ''}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 20f6f99. selectedRealm is now a getter (_explicitlySelectedRealm ?? initialRealm) — equivalent to the effective-realm fallback you suggested, applied centrally so every consumer (yielded tree, upload, drop, drop-zone label, selectedRealmURL) tracks the realm list as it loads.

Comment on lines +108 to +113
private get dropZoneLabel(): string {
if (!this.selectedRealm) {
return '';
}
return `Drop file to upload to ${this.selectedRealm.info.name}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 20f6f99. dropZoneLabel reads this.selectedRealm, which is now the effective-realm getter, so the label fills in once the realm list loads.

Comment on lines +136 to +146
@action
private triggerUpload() {
if (!this.selectedRealm) {
return;
}
let task = this.fileUpload.uploadFile({
realmURL: this.selectedRealm.url,
acceptTypes: this.args.acceptTypes,
});
this.beginUpload(task);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 20f6f99. triggerUpload reads this.selectedRealm via the new getter, so the button no longer becomes a no-op when realms arrive after mount.

Comment on lines +198 to +209
if (this.isUploadBusy || !this.selectedRealm) {
return;
}
let file = dragEvent.dataTransfer?.files?.[0];
if (!file) {
return;
}
let task = this.fileUpload.uploadProvidedFile({
realmURL: this.selectedRealm.url,
file,
});
this.beginUpload(task);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 20f6f99. handleDrop reads this.selectedRealm via the new getter, so drops are honored as soon as the effective realm is available.

Comment on lines +248 to +251
fileTreeKey=this.fileTreeKey
selectedRealm=this.selectedRealm
selectedRealmURL=this.selectedRealmURL
currentUpload=this.currentUpload

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 20f6f99. The yielded selectedRealm is the getter (_explicitlySelectedRealm ?? initialRealm), so MiniFileChooser / FileChooserModal see the effective realm and render the tree once realms load.

Comment on lines 186 to +194
get selectedRealm(): RealmDropdownItem | undefined {
// Until the realm list has loaded there is nothing to select, and the
// defaultWritableRealm fallback below reaches into the matrix client —
// which throws if it's consulted before the matrix SDK is ready (e.g. while
// this dropdown renders during app boot). The fallback would resolve to
// undefined against an empty list anyway, so short-circuit.
if (this.realms.length === 0) {
return undefined;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 20f6f99. selectedItemText now short-circuits to \' when selectedRealm is undefined, so the title and label stay empty during boot instead of rendering "In undefined".

FadhlanR and others added 3 commits June 24, 2026 09:27
…ter, realm-dropdown guard

- panel.gts: Replace snapshot-initialized selectedRealm with getter/setter over
  _explicitlySelectedRealm, falling through to initialRealm reactively. Fixes
  Issues 2-6 where selectedRealm stays undefined when realms load async.
- realm-dropdown.gts: Guard selectedItemText against undefined selectedRealm.
  Fixes Issue 7 (cosmetic "In undefined").
- mini/index.gts: Add handleFileSelectOnly action for onFileSelected, keeping
  onFileConfirmed as the sole trigger of onSelect. Fixes Issue 1 (double-fire
  on Enter).
The fix for Issue 1 (double-fire on Enter) changed click to only update
visual selection (handleFileSelectOnly), not fire onSelect. The test
needs to click the file then press Enter to confirm the selection.
Plan doc that informed the review-feedback fixes — drop it from the
merged history per the workflow note about planning artifacts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants